chualinwei3 commited on
Commit
f742815
·
verified ·
1 Parent(s): 78fbb46

Upload 30 files

Browse files
.gitignore ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # .gitignore for Attendr Smart Attendance System
2
+
3
+ # Python
4
+ __pycache__/
5
+ *.py[cod]
6
+ *$py.class
7
+ *.so
8
+ .Python
9
+ build/
10
+ develop-eggs/
11
+ dist/
12
+ downloads/
13
+ eggs/
14
+ .eggs/
15
+ lib/
16
+ lib64/
17
+ parts/
18
+ sdist/
19
+ var/
20
+ wheels/
21
+ *.egg-info/
22
+ .installed.cfg
23
+ *.egg
24
+
25
+ # Virtual Environment
26
+ venv/
27
+ ENV/
28
+ env/
29
+
30
+ # Database
31
+ *.db
32
+ *.sqlite
33
+ *.sqlite3
34
+ attendr.db
35
+
36
+ # Uploads and User Data
37
+ uploads/
38
+ face_encodings/
39
+
40
+ # IDE
41
+ .vscode/
42
+ .idea/
43
+ *.swp
44
+ *.swo
45
+ *~
46
+
47
+ # OS
48
+ .DS_Store
49
+ Thumbs.db
50
+
51
+ # Environment variables
52
+ .env
53
+ .env.local
54
+
55
+ # Logs
56
+ *.log
57
+
58
+ # Flask
59
+ instance/
60
+ .webassets-cache
61
+
62
+ # Testing
63
+ .pytest_cache/
64
+ .coverage
65
+ htmlcov/
README.md CHANGED
@@ -1,10 +1,338 @@
1
- ---
2
- title: Attender
3
- emoji: 🦀
4
- colorFrom: red
5
- colorTo: yellow
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Attendr - Smart Attendance System 🎓
2
+
3
+ An AI-powered smart attendance system for Universiti Teknologi Malaysia (UTM) that combines **facial recognition**, **geolocation verification**, and **auto-refresh attendance codes** to eliminate proxy attendance and streamline the attendance process.
4
+
5
+ ![Python](https://img.shields.io/badge/Python-3.8+-blue.svg)
6
+ ![Flask](https://img.shields.io/badge/Flask-3.0-green.svg)
7
+ ![License](https://img.shields.io/badge/License-MIT-yellow.svg)
8
+
9
+ ## ✨ Features
10
+
11
+ ### For Students
12
+ - 🎭 **Biometric Face Recognition** - Secure identity verification using AI
13
+ - 📍 **GPS Location Validation** - Confirms physical presence in classroom
14
+ - 🔐 **Auto-Refresh Codes** - Time-limited codes prevent sharing
15
+ - ⚡ **Real-time Feedback** - Instant attendance confirmation
16
+ - 📱 **Mobile Friendly** - Works on smartphones and tablets
17
+
18
+ ### For Lecturers
19
+ - 🎯 **One-Click Session Creation** - Quick setup for any class
20
+ - 🔄 **Auto-Refreshing Codes** - New code every 2 minutes
21
+ - 📊 **Real-time Monitoring** - Live attendance updates
22
+ - 📥 **Export Reports** - Download CSV for record-keeping
23
+ - 📈 **Attendance Analytics** - View statistics at a glance
24
+
25
+ ### Security Features
26
+ - ✅ Prevents proxy attendance through face + location verification
27
+ - ✅ Time-limited codes expire automatically
28
+ - ✅ All verifications logged with timestamps
29
+ - ✅ Distance tracking from classroom center
30
+
31
+ ## 🧠 AI Logic & Knowledge Representation
32
+
33
+ Attendr implements **5 Knowledge Representation (KR) rules** using First-Order Logic:
34
+
35
+ 1. **Face Recognition**: `∀x ((CapturedFace(x) ∧ MatchStored(x)) → FaceMatch(x))`
36
+ 2. **Location Verification**: `∀x[(Student(x) ∧ IsWithinAllowedArea(x)) → VerifiedLocation(x)]`
37
+ 3. **Device Readiness**: `∀d ((CameraOn(d) ∧ GPSOn(d)) → StartVerification(d))`
38
+ 4. **Attendance Validation**: `∀x ((FaceMatch(x) ∧ LocationValid(x)) → GrantCodeAccess(x))`
39
+ 5. **Code Confirmation**: `∀x ((ValidCodeEntry(User,x) ∧ WithinCycle(Code,x)) → MarkPresent(System,x))`
40
+
41
+ ## 🚀 Installation
42
+
43
+ ### Prerequisites
44
+ - Python 3.8 or higher
45
+ - Webcam for face capture
46
+ - GPS-enabled device (or browser location services)
47
+
48
+ ### Step 1: Clone Repository
49
+ ```bash
50
+ git clone <repository-url>
51
+ cd smartAttandence
52
+ ```
53
+
54
+ ### Step 2: Install Dependencies
55
+
56
+ #### Windows (Recommended Method)
57
+ ```powershell
58
+ # Install Visual C++ Build Tools first (required for dlib)
59
+ # Download from: https://visualstudio.microsoft.com/visual-cpp-build-tools/
60
+
61
+ # Create virtual environment
62
+ python -m venv venv
63
+ venv\Scripts\activate
64
+
65
+ # Install dependencies
66
+ pip install -r requirements.txt
67
+ ```
68
+
69
+ #### Alternative: Use Pre-compiled Wheels
70
+ If you encounter issues installing `face_recognition`, use pre-compiled wheels:
71
+ ```powershell
72
+ pip install https://github.com/jloh02/dlib/releases/download/v19.22/dlib-19.22.99-cp38-cp38-win_amd64.whl
73
+ pip install face_recognition
74
+ ```
75
+
76
+ ### Step 3: Initialize Database
77
+ ```bash
78
+ python init_db.py
79
+ ```
80
+
81
+ This creates:
82
+ - SQLite database with all tables
83
+ - Sample UTM classrooms (N28, V01, C22)
84
+ - Test student account
85
+
86
+ ### Step 4: Run Application
87
+ ```bash
88
+ python app.py
89
+ ```
90
+
91
+ The application will be available at: **http://localhost:5000**
92
+
93
+ ## 📖 Usage Guide
94
+
95
+ ### For Students
96
+
97
+ 1. **Register Your Face**
98
+ - Visit `/register`
99
+ - Enter Student ID and Name
100
+ - Capture your face using webcam
101
+ - System stores your biometric encoding
102
+
103
+ 2. **Mark Attendance**
104
+ - Visit `/student`
105
+ - Enter Student ID and select active session
106
+ - Enable camera and GPS permissions
107
+ - System verifies your face and location
108
+ - Enter attendance code displayed by lecturer
109
+ - Attendance marked instantly!
110
+
111
+ ### For Lecturers
112
+
113
+ 1. **Create Session**
114
+ - Visit `/lecturer`
115
+ - Enter course name and your name
116
+ - Select classroom location
117
+ - Click "Create Session"
118
+
119
+ 2. **Display Code**
120
+ - Large attendance code appears on screen
121
+ - Code auto-refreshes every 2 minutes
122
+ - Show this code to students in class
123
+
124
+ 3. **Monitor Attendance**
125
+ - View real-time attendance list
126
+ - See verification details (distance, time)
127
+ - Export CSV report when done
128
+
129
+ ## 🗂️ Project Structure
130
+
131
+ ```
132
+ smartAttandence/
133
+ ├── app.py # Main Flask application
134
+ ├── config.py # Configuration settings
135
+ ├── models.py # Database models
136
+ ├── init_db.py # Database initialization
137
+ ├── requirements.txt # Python dependencies
138
+
139
+ ├── face_recognition_module.py # Face recognition logic (KR Rule #1)
140
+ ├── geolocation_module.py # GPS verification (KR Rule #2)
141
+ ├── attendance_code_module.py # Auto-refresh codes (KR Rule #5)
142
+ ├── utils.py # Helper functions
143
+
144
+ ├── static/
145
+ │ ├── css/
146
+ │ │ └── style.css # Premium design system
147
+ │ └── js/
148
+ │ ├── student.js # Student portal logic
149
+ │ └── lecturer.js # Lecturer dashboard logic
150
+
151
+ └── templates/
152
+ ├── index.html # Landing page
153
+ ├── student.html # Student portal
154
+ ├── lecturer.html # Lecturer dashboard
155
+ └── register.html # Face registration
156
+ ```
157
+
158
+ ## 🔧 Configuration
159
+
160
+ Edit `config.py` to customize:
161
+
162
+ ```python
163
+ # Face recognition settings
164
+ FACE_RECOGNITION_TOLERANCE = 0.6 # Lower = more strict (0.4-0.6 recommended)
165
+
166
+ # Geolocation settings
167
+ GEOLOCATION_RADIUS_METERS = 50 # Classroom detection radius
168
+
169
+ # Attendance code settings
170
+ CODE_REFRESH_INTERVAL_SECONDS = 120 # Code refresh interval (2 minutes)
171
+ CODE_LENGTH = 6 # Length of attendance code
172
+ ```
173
+
174
+ ## 🌐 API Endpoints
175
+
176
+ ### Student Endpoints
177
+ - `POST /api/register_face` - Register student face encoding
178
+ - `POST /api/verify_face` - Verify face against stored encoding
179
+ - `POST /api/verify_location` - Validate GPS location
180
+ - `POST /api/mark_attendance` - Mark attendance with code
181
+
182
+ ### Lecturer Endpoints
183
+ - `POST /api/create_session` - Create attendance session
184
+ - `GET /api/get_session/<id>` - Get session details with current code
185
+ - `GET /api/get_attendance/<id>` - Get attendance records
186
+ - `POST /api/end_session/<id>` - End session
187
+
188
+ ### General Endpoints
189
+ - `GET /api/get_active_sessions` - List all active sessions
190
+ - `GET /api/classrooms` - Get all classrooms
191
+
192
+ ## 🧪 Testing
193
+
194
+ ### Manual Testing Checklist
195
+
196
+ **Face Recognition:**
197
+ - [ ] Register new student face
198
+ - [ ] Verify with same person (should succeed)
199
+ - [ ] Verify with different person (should fail)
200
+ - [ ] Test with poor lighting
201
+ - [ ] Test with glasses/mask
202
+
203
+ **Geolocation:**
204
+ - [ ] Mark attendance from inside classroom (should succeed)
205
+ - [ ] Mark attendance from outside radius (should fail)
206
+ - [ ] Verify distance calculation accuracy
207
+
208
+ **Auto-Refresh Codes:**
209
+ - [ ] Code refreshes every 2 minutes
210
+ - [ ] Expired code rejected
211
+ - [ ] Valid code accepted
212
+ - [ ] Code sharing prevented
213
+
214
+ **End-to-End Flow:**
215
+ - [ ] Complete student registration
216
+ - [ ] Lecturer creates session
217
+ - [ ] Student marks attendance successfully
218
+ - [ ] Attendance appears in real-time
219
+ - [ ] Export CSV works
220
+
221
+ ## 🐛 Troubleshooting
222
+
223
+ ### Face Recognition Issues
224
+
225
+ **Problem:** `dlib` installation fails on Windows
226
+ **Solution:**
227
+ 1. Install Visual C++ Build Tools
228
+ 2. Or use pre-compiled wheel: `pip install dlib-19.22.99-cp38-cp38-win_amd64.whl`
229
+
230
+ **Problem:** "No face detected"
231
+ **Solution:**
232
+ - Ensure good lighting
233
+ - Face camera directly
234
+ - Remove glasses/mask if possible
235
+ - Move closer to camera
236
+
237
+ ### GPS Issues
238
+
239
+ **Problem:** Location permission denied
240
+ **Solution:**
241
+ - Enable location services in browser settings
242
+ - Use HTTPS (required for geolocation API)
243
+ - Check device GPS is enabled
244
+
245
+ **Problem:** "Location unavailable"
246
+ **Solution:**
247
+ - Ensure GPS is enabled on device
248
+ - Try outdoors for better signal
249
+ - Check browser location permissions
250
+
251
+ ### Code Validation Issues
252
+
253
+ **Problem:** "Code has expired"
254
+ **Solution:**
255
+ - Enter code within 2-minute window
256
+ - Check lecturer's displayed code
257
+ - Ensure system clocks are synchronized
258
+
259
+ ## 📊 Database Schema
260
+
261
+ ### Students Table
262
+ - `id` - Primary key
263
+ - `student_id` - Unique student identifier
264
+ - `name` - Student name
265
+ - `email` - Email address
266
+ - `face_encoding` - Biometric data (JSON)
267
+ - `registered_at` - Registration timestamp
268
+
269
+ ### Classrooms Table
270
+ - `id` - Primary key
271
+ - `name` - Classroom name (e.g., N28-01-01)
272
+ - `building` - Building name
273
+ - `latitude` - GPS latitude
274
+ - `longitude` - GPS longitude
275
+ - `radius_meters` - Geofencing radius
276
+
277
+ ### AttendanceSessions Table
278
+ - `id` - Primary key
279
+ - `course_name` - Course name
280
+ - `lecturer_name` - Lecturer name
281
+ - `classroom_id` - Foreign key to Classrooms
282
+ - `current_code` - Active attendance code
283
+ - `code_generated_at` - Code generation time
284
+ - `is_active` - Session status
285
+
286
+ ### AttendanceRecords Table
287
+ - `id` - Primary key
288
+ - `session_id` - Foreign key to AttendanceSessions
289
+ - `student_id` - Foreign key to Students
290
+ - `marked_at` - Attendance timestamp
291
+ - `face_verified` - Face verification status
292
+ - `location_verified` - Location verification status
293
+ - `code_verified` - Code verification status
294
+ - `distance_from_classroom` - Distance in meters
295
+
296
+ ## 🎨 Design Philosophy
297
+
298
+ Attendr features a **premium dark mode design** with:
299
+ - 🌈 Vibrant gradient accents
300
+ - ✨ Glassmorphism effects
301
+ - 🎭 Smooth micro-animations
302
+ - 📱 Fully responsive layout
303
+ - 🎯 Modern typography (Inter + Outfit fonts)
304
+
305
+ ## 🤝 Contributing
306
+
307
+ Contributions are welcome! Please follow these steps:
308
+ 1. Fork the repository
309
+ 2. Create a feature branch
310
+ 3. Make your changes
311
+ 4. Test thoroughly
312
+ 5. Submit a pull request
313
+
314
+ ## 📄 License
315
+
316
+ This project is licensed under the MIT License.
317
+
318
+ ## 👥 Authors
319
+
320
+ Developed for Universiti Teknologi Malaysia (UTM) as part of an AI Smart Attendance System project.
321
+
322
+ ## 🙏 Acknowledgments
323
+
324
+ - **OpenCV** - Computer vision library
325
+ - **face_recognition** - Face recognition library by Adam Geitgey
326
+ - **Flask** - Web framework
327
+ - **UTM** - Project inspiration and requirements
328
+
329
+ ## 📞 Support
330
+
331
+ For issues or questions:
332
+ 1. Check the Troubleshooting section
333
+ 2. Review API documentation
334
+ 3. Open an issue on GitHub
335
+
336
+ ---
337
+
338
+ **Made with ❤️ for UTM Students and Lecturers**
_navbar.html ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- Unified Navigation Bar Component -->
2
+ <!-- Copy this to all pages: index.html, login.html, register.html, student.html, student_history.html, lecturer.html -->
3
+
4
+ <nav class="navbar">
5
+ <div class="container navbar-content">
6
+ <a href="/" class="navbar-brand">✨ Attendr</a>
7
+ <ul class="navbar-nav">
8
+ <li><a href="/" class="navbar-link">Home</a></li>
9
+ {% if current_user %}
10
+ {% if current_user.role == 'student' %}
11
+ <li><a href="/student" class="navbar-link">Mark Attendance</a></li>
12
+ <li><a href="/student/history" class="navbar-link">History</a></li>
13
+ {% elif current_user.role == 'lecturer' %}
14
+ <li><a href="/lecturer" class="navbar-link">Dashboard</a></li>
15
+ {% endif %}
16
+ <li><span class="navbar-link" style="color: var(--color-primary);">👤 {{ current_user.name }}</span></li>
17
+ <li><a href="#" onclick="logout()" class="navbar-link">Logout</a></li>
18
+ {% else %}
19
+ <li><a href="/login" class="navbar-link">Login</a></li>
20
+ <li><a href="/register" class="navbar-link">Register</a></li>
21
+ {% endif %}
22
+ </ul>
23
+ </div>
24
+ </nav>
25
+
26
+ <!-- Logout Script (add to pages that need it) -->
27
+ <script>
28
+ async function logout() {
29
+ if (!confirm('Are you sure you want to logout?')) return;
30
+
31
+ try {
32
+ const response = await fetch('/api/auth/logout', {
33
+ method: 'POST',
34
+ headers: { 'Content-Type': 'application/json' }
35
+ });
36
+
37
+ const data = await response.json();
38
+
39
+ if (data.success) {
40
+ alert(data.message);
41
+ window.location.href = '/';
42
+ } else {
43
+ alert('Logout failed');
44
+ }
45
+ } catch (error) {
46
+ alert('Logout failed');
47
+ }
48
+ }
49
+ </script>
add_home_venue.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Add Puteri Court Condominium as a test venue for home demo
3
+ Run this script to add your home location to the database
4
+ """
5
+
6
+ from app import app, db
7
+ from models import Classroom
8
+
9
+ def add_puteri_court():
10
+ """Add Puteri Court Condominium as a test venue"""
11
+
12
+ with app.app_context():
13
+ print("Adding Puteri Court Condominium as test venue...")
14
+
15
+ # Check if already exists
16
+ existing = Classroom.query.filter_by(name='PUTERI-COURT').first()
17
+
18
+ if existing:
19
+ print(f"Puteri Court already exists. Updating coordinates...")
20
+ # Updated to correct Puteri Court location (Ampang/Pandan Mewah)
21
+ # Address: Jalan Pandan Mewah, Taman Pandan Mewah, 68000 Kuala Lumpur
22
+ existing.latitude = 3.128024
23
+ existing.longitude = 101.765479
24
+ existing.radius_meters = 1000 # 1km radius for easier testing
25
+ db.session.commit()
26
+ print(f"[OK] Updated Puteri Court")
27
+ else:
28
+ # Create new venue with correct Puteri Court location
29
+ puteri_court = Classroom(
30
+ name='PUTERI-COURT',
31
+ building='Puteri Court Condominium (Home Demo)',
32
+ latitude=3.128024, # Taman Pandan Mewah, Ampang
33
+ longitude=101.765479, # Taman Pandan Mewah, Ampang
34
+ radius_meters=1000 # 1km radius for easier testing
35
+ )
36
+
37
+ db.session.add(puteri_court)
38
+ db.session.commit()
39
+ print(f"[OK] Added Puteri Court Condominium")
40
+
41
+ # Display the venue details
42
+ venue = Classroom.query.filter_by(name='PUTERI-COURT').first()
43
+ print("\n" + "="*70)
44
+ print("Test Venue Added Successfully!")
45
+ print("="*70)
46
+ print(f"Venue Code: {venue.name}")
47
+ print(f"Building: {venue.building}")
48
+ print(f"Latitude: {venue.latitude}")
49
+ print(f"Longitude: {venue.longitude}")
50
+ print(f"Radius: {venue.radius_meters} meters")
51
+ print("="*70)
52
+ print("\nIMPORTANT: To get your exact GPS coordinates:")
53
+ print("1. Open Google Maps on your phone")
54
+ print("2. Long-press on your location")
55
+ print("3. Copy the coordinates (e.g., 1.4927, 103.7414)")
56
+ print("4. Update this script with your actual coordinates")
57
+ print("5. Run this script again to update")
58
+ print("\nOr you can update directly in the database:")
59
+ print(f"UPDATE classrooms SET latitude=YOUR_LAT, longitude=YOUR_LON WHERE name='PUTERI-COURT';")
60
+ print("="*70)
61
+
62
+ # Show total venues
63
+ total = Classroom.query.count()
64
+ print(f"\nTotal venues in database: {total}")
65
+
66
+ if __name__ == '__main__':
67
+ add_puteri_court()
add_utm_venues.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Add UTM Faculty of Computing Venues to Database
3
+ This script adds all FC classrooms and labs with their GPS coordinates
4
+ """
5
+
6
+ from app import app, db
7
+ from models import Classroom
8
+
9
+ # UTM Skudai approximate coordinates for Faculty of Computing (N28)
10
+ # Base coordinates: 1.5586°N, 103.6376°E
11
+ # Each classroom will have slightly different coordinates
12
+
13
+ def add_utm_venues():
14
+ """Add all UTM FC venues to the database"""
15
+
16
+ with app.app_context():
17
+ print("Adding UTM Faculty of Computing venues...")
18
+
19
+ # Define all venues with their details
20
+ venues = [
21
+ # N28 Main Building - Lecture Halls (Bilik Kuliah)
22
+ {"code": "N28-105-01", "name": "Bilik Kuliah 1 (BK1)", "building": "N28 - Faculty of Computing", "lat": 1.5586, "lon": 103.6376, "capacity": 120},
23
+ {"code": "N28-116-02", "name": "Bilik Kuliah 2 (BK2)", "building": "N28 - Faculty of Computing", "lat": 1.5587, "lon": 103.6377, "capacity": 120},
24
+ {"code": "N28-112-02", "name": "Bilik Kuliah 3 (BK3)", "building": "N28 - Faculty of Computing", "lat": 1.5588, "lon": 103.6378, "capacity": 120},
25
+ {"code": "N28-111-01", "name": "Bilik Kuliah 4 (BK4)", "building": "N28 - Faculty of Computing", "lat": 1.5589, "lon": 103.6379, "capacity": 120},
26
+ {"code": "N28-106-01", "name": "Bilik Kuliah 5 (BK5)", "building": "N28 - Faculty of Computing", "lat": 1.5590, "lon": 103.6380, "capacity": 60},
27
+ {"code": "N28-107-01", "name": "Bilik Kuliah 6 (BK6)", "building": "N28 - Faculty of Computing", "lat": 1.5591, "lon": 103.6381, "capacity": 120},
28
+ {"code": "N28-108-01", "name": "Bilik Kuliah 7 (BK7)", "building": "N28 - Faculty of Computing", "lat": 1.5592, "lon": 103.6382, "capacity": 120},
29
+
30
+ # N28 Level 3 - Special Labs
31
+ {"code": "N28-351-02", "name": "Active Learning Lab (ALL)", "building": "N28 - Level 3", "lat": 1.5593, "lon": 103.6383, "capacity": 36},
32
+ {"code": "N28-301-01", "name": "PMO", "building": "N28 - Level 3", "lat": 1.5594, "lon": 103.6384, "capacity": 36},
33
+ {"code": "N28-301-01B", "name": "Bilik Mesyuarat PMO (BMPMO)", "building": "N28 - Level 3", "lat": 1.5595, "lon": 103.6385, "capacity": 30},
34
+
35
+ # N28A Building - Tutorial Rooms
36
+ {"code": "N28A-BT1", "name": "Bilik Tutorial 1 (BT1)", "building": "N28A - Tutorial Block", "lat": 1.5596, "lon": 103.6386, "capacity": 24},
37
+ {"code": "N28A-BT2", "name": "Bilik Tutorial 2 (BT2)", "building": "N28A - Tutorial Block", "lat": 1.5597, "lon": 103.6387, "capacity": 27},
38
+ {"code": "N28A-BT3", "name": "Bilik Tutorial 3 (BT3)", "building": "N28A - Tutorial Block", "lat": 1.5598, "lon": 103.6388, "capacity": 24},
39
+ {"code": "N28A-BT4", "name": "Bilik Tutorial 4 (BT4)", "building": "N28A - Tutorial Block", "lat": 1.5599, "lon": 103.6389, "capacity": 40},
40
+ {"code": "N28A-BK1", "name": "Bilik Kuliah 1 (BK1)", "building": "N28A - Tutorial Block", "lat": 1.5600, "lon": 103.6390, "capacity": 28},
41
+ {"code": "N28A-BK2", "name": "Bilik Kuliah 2 (BK2)", "building": "N28A - Tutorial Block", "lat": 1.5601, "lon": 103.6391, "capacity": 36},
42
+ {"code": "N28A-BK5", "name": "Bilik Kuliah 5 (BK5)", "building": "N28A - Tutorial Block", "lat": 1.5602, "lon": 103.6392, "capacity": 28},
43
+ {"code": "N28A-BK6", "name": "Bilik Kuliah 6 (BK6)", "building": "N28A - Tutorial Block", "lat": 1.5603, "lon": 103.6393, "capacity": 36},
44
+
45
+ # N28 Level 5 - Computer Programming Labs (Makmal Pengaturcaraan Komputer)
46
+ {"code": "N28-502-01", "name": "Makmal Pengaturcaraan Komputer 1 (MPK 1)", "building": "N28 - Level 5", "lat": 1.5604, "lon": 103.6394, "capacity": 60},
47
+ {"code": "N28-505-01", "name": "Makmal Pengaturcaraan Komputer 2 (MPK 2)", "building": "N28 - Level 5", "lat": 1.5605, "lon": 103.6395, "capacity": 60},
48
+ {"code": "N28-506-01", "name": "Makmal Pengaturcaraan Komputer 3 (MPK 3)", "building": "N28 - Level 5", "lat": 1.5606, "lon": 103.6396, "capacity": 60},
49
+ {"code": "N28-512-01", "name": "Makmal Pengaturcaraan Komputer 4 (MPK 4)", "building": "N28 - Level 5", "lat": 1.5607, "lon": 103.6397, "capacity": 60},
50
+ {"code": "N28-516-01", "name": "Makmal Pengaturcaraan Komputer 5 (MPK 5)", "building": "N28 - Level 5", "lat": 1.5608, "lon": 103.6398, "capacity": 25},
51
+ {"code": "N28-517-02", "name": "Makmal Pengaturcaraan Komputer 6 (MPK 6)", "building": "N28 - Level 5", "lat": 1.5609, "lon": 103.6399, "capacity": 60},
52
+ {"code": "N28-518-02", "name": "Makmal Pengaturcaraan Komputer 7 (MPK 7)", "building": "N28 - Level 5", "lat": 1.5610, "lon": 103.6400, "capacity": 60},
53
+ {"code": "N28-524-02", "name": "Makmal Pengaturcaraan Komputer 8 (MPK 8)", "building": "N28 - Level 5", "lat": 1.5611, "lon": 103.6401, "capacity": 60},
54
+ {"code": "N28-525-01", "name": "Makmal Pengaturcaraan Komputer 9 (MPK 9)", "building": "N28 - Level 5", "lat": 1.5612, "lon": 103.6402, "capacity": 60},
55
+ {"code": "N28-527-02", "name": "Makmal Pengaturcaraan Komputer 10 (MPK 10)", "building": "N28 - Level 5", "lat": 1.5613, "lon": 103.6403, "capacity": 60},
56
+
57
+ # N28A Level 2 - Teaching Labs
58
+ {"code": "N28A-02-33", "name": "Makmal Pengajaran 1 (MP1-N28A)", "building": "N28A - Level 2", "lat": 1.5614, "lon": 103.6404, "capacity": 48},
59
+ {"code": "N28A-02-34", "name": "Makmal Pengajaran 2 (MP2-N28A)", "building": "N28A - Level 2", "lat": 1.5615, "lon": 103.6405, "capacity": 48},
60
+
61
+ # N28 Level 2 - Specialized Labs
62
+ {"code": "N28-223-02", "name": "Computer Graphic & Multimedia Teaching Lab (CGMTL)", "building": "N28 - Level 2", "lat": 1.5616, "lon": 103.6406, "capacity": 40},
63
+ {"code": "N28-204-01", "name": "Computer Vision Lab (CVL)", "building": "N28 - Level 2", "lat": 1.5617, "lon": 103.6407, "capacity": 30},
64
+ {"code": "N28-203-01", "name": "Computer Vision Teaching Lab (CVTL)", "building": "N28 - Level 2", "lat": 1.5618, "lon": 103.6408, "capacity": 30},
65
+ {"code": "N28-202-01", "name": "Virtual Environment Teaching Lab (VETL)", "building": "N28 - Level 2", "lat": 1.5619, "lon": 103.6409, "capacity": 25},
66
+
67
+ # N28 Level 3 - Research Labs
68
+ {"code": "N28-330-01", "name": "Computer Security Lab (CSL)", "building": "N28 - Level 3", "lat": 1.5620, "lon": 103.6410, "capacity": 35},
69
+ {"code": "N28-352-01", "name": "CCNA Teaching Lab (CCNA)", "building": "N28 - Level 3", "lat": 1.5621, "lon": 103.6411, "capacity": 30},
70
+ {"code": "N28-321-02", "name": "Makmal CASE (MCASE)", "building": "N28 - Level 3", "lat": 1.5622, "lon": 103.6412, "capacity": 28},
71
+ {"code": "N28-329-01", "name": "Information System Teaching Lab (ISTL)", "building": "N28 - Level 3", "lat": 1.5623, "lon": 103.6413, "capacity": 26},
72
+ {"code": "N28-350-03", "name": "Data-Analytics Lab (DAL)", "building": "N28 - Level 3", "lat": 1.5624, "lon": 103.6414, "capacity": 23},
73
+
74
+ # N28 Level 4 - Advanced Labs
75
+ {"code": "N28-422-01", "name": "CCNP Lab (CCNP)", "building": "N28 - Level 4", "lat": 1.5625, "lon": 103.6415, "capacity": 36},
76
+ {"code": "N28-423-01", "name": "Makmal IDAL (IDAL)", "building": "N28 - Level 4", "lat": 1.5626, "lon": 103.6416, "capacity": 32},
77
+
78
+ # Other Campus Buildings - Large Lecture Halls
79
+ {"code": "L50-DK3", "name": "Dewan Kuliah 3 (L50-DK3)", "building": "Block L50 - Centre Point", "lat": 1.5580, "lon": 103.6370, "capacity": 250},
80
+ {"code": "P19-DK4", "name": "Dewan Kuliah 4 (P19-DK4)", "building": "Block P19 - FKE", "lat": 1.5575, "lon": 103.6365, "capacity": 250},
81
+ {"code": "P19-DK5", "name": "Dewan Kuliah 5 (P19-DK5)", "building": "Block P19 - FKE", "lat": 1.5574, "lon": 103.6364, "capacity": 250},
82
+ {"code": "P19-DK6", "name": "Dewan Kuliah 6 (P19-DK6)", "building": "Block P19 - FKE", "lat": 1.5573, "lon": 103.6363, "capacity": 250},
83
+ ]
84
+
85
+ # Add all venues to database
86
+ added_count = 0
87
+ updated_count = 0
88
+
89
+ for venue in venues:
90
+ # Check if venue already exists
91
+ existing = Classroom.query.filter_by(name=venue['code']).first()
92
+
93
+ if existing:
94
+ # Update existing venue
95
+ existing.building = venue['building']
96
+ existing.latitude = venue['lat']
97
+ existing.longitude = venue['lon']
98
+ existing.radius_meters = 50 # Default 50m radius
99
+ updated_count += 1
100
+ print(f"Updated: {venue['code']} - {venue['name']}")
101
+ else:
102
+ # Add new venue
103
+ classroom = Classroom(
104
+ name=venue['code'],
105
+ building=venue['building'],
106
+ latitude=venue['lat'],
107
+ longitude=venue['lon'],
108
+ radius_meters=50 # Default 50m radius for all classrooms
109
+ )
110
+ db.session.add(classroom)
111
+ added_count += 1
112
+ print(f"Added: {venue['code']} - {venue['name']}")
113
+
114
+ # Commit all changes
115
+ db.session.commit()
116
+
117
+ print("\n" + "="*70)
118
+ print(f"Venue import complete!")
119
+ print(f"Added: {added_count} new venues")
120
+ print(f"Updated: {updated_count} existing venues")
121
+ print(f"Total venues in database: {Classroom.query.count()}")
122
+ print("="*70)
123
+
124
+ # Display summary by building
125
+ print("\nVenues by Building:")
126
+ buildings = db.session.query(Classroom.building).distinct().all()
127
+ for (building,) in buildings:
128
+ count = Classroom.query.filter_by(building=building).count()
129
+ print(f" - {building}: {count} venues")
130
+
131
+ if __name__ == '__main__':
132
+ add_utm_venues()
app.py ADDED
@@ -0,0 +1,791 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Attendr - Smart Attendance System
3
+ Main Flask Application
4
+ """
5
+
6
+ from flask import Flask, render_template, request, jsonify, session, redirect, url_for, flash, send_from_directory
7
+ from flask_sqlalchemy import SQLAlchemy
8
+ from config import Config
9
+ from models import db, User, Student, Classroom, AttendanceSession, AttendanceRecord
10
+ from face_recognition_module import face_recognition_module
11
+ from geolocation_module import geolocation_module
12
+ from attendance_code_module import attendance_code_module
13
+ from utils import base64_to_image, image_to_numpy, format_error_response, format_success_response
14
+ from auth import login_required, role_required, get_current_user
15
+ from datetime import datetime
16
+ import logging
17
+
18
+ # Initialize Flask app
19
+ app = Flask(__name__, template_folder='.')
20
+ app.config.from_object(Config)
21
+ Config.init_app(app)
22
+
23
+ # Initialize database
24
+ db.init_app(app)
25
+
26
+ # Set up logging
27
+ logging.basicConfig(level=logging.INFO)
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ # ==================== ROUTES ====================
32
+
33
+ @app.route('/style.css')
34
+ def style():
35
+ return send_from_directory('.', 'style.css')
36
+
37
+
38
+ @app.route('/')
39
+ def index():
40
+ """Landing page"""
41
+ current_user = get_current_user()
42
+ return render_template('index.html', current_user=current_user)
43
+
44
+
45
+ @app.route('/student')
46
+ @login_required
47
+ @role_required('student')
48
+ def student_page():
49
+ """Student attendance interface - requires student login"""
50
+ return render_template('student.html', current_user=get_current_user())
51
+
52
+
53
+ @app.route('/student/history')
54
+ @login_required
55
+ @role_required('student')
56
+ def student_history():
57
+ """Student attendance history page"""
58
+ return render_template('student_history.html', current_user=get_current_user())
59
+
60
+
61
+ @app.route('/lecturer')
62
+ @login_required
63
+ @role_required('lecturer')
64
+ def lecturer_page():
65
+ """Lecturer dashboard - requires lecturer login"""
66
+ return render_template('lecturer.html', current_user=get_current_user())
67
+
68
+
69
+ @app.route('/lecturer/history')
70
+ @login_required
71
+ @role_required('lecturer')
72
+ def lecturer_history():
73
+ """Lecturer session history page"""
74
+ return render_template('lecturer_history.html', current_user=get_current_user())
75
+
76
+
77
+ @app.route('/register')
78
+ def register_page():
79
+ """User registration page"""
80
+ return render_template('register.html')
81
+
82
+
83
+ @app.route('/login')
84
+ def login_page():
85
+ """Login page"""
86
+ return render_template('login.html')
87
+
88
+
89
+ # ==================== AUTHENTICATION API ====================
90
+
91
+ @app.route('/api/auth/register', methods=['POST'])
92
+ def register_user():
93
+ """
94
+ Register new user (student or lecturer)
95
+ Validates matric number format based on role
96
+ """
97
+ try:
98
+ data = request.get_json()
99
+
100
+ required_fields = ['matric_no', 'name', 'password', 'role']
101
+ if not all(k in data for k in required_fields):
102
+ return format_error_response(f"Missing required fields: {', '.join(required_fields)}")
103
+
104
+ matric_no = data['matric_no'].upper().strip()
105
+ name = data['name']
106
+ password = data['password']
107
+ role = data['role'].lower()
108
+ email = data.get('email')
109
+
110
+ # Validate role
111
+ if role not in ['student', 'lecturer']:
112
+ return format_error_response("Role must be 'student' or 'lecturer'")
113
+
114
+ # Validate matric number format
115
+ is_valid, error_msg = User.validate_matric_no(matric_no, role)
116
+ if not is_valid:
117
+ return format_error_response(error_msg, 400)
118
+
119
+ # Check if user already exists
120
+ existing_user = User.query.filter_by(matric_no=matric_no).first()
121
+ if existing_user:
122
+ return format_error_response("Matric number already registered", 409)
123
+
124
+ if email:
125
+ existing_email = User.query.filter_by(email=email).first()
126
+ if existing_email:
127
+ return format_error_response("Email already registered", 409)
128
+
129
+ # Create user
130
+ user = User(
131
+ matric_no=matric_no,
132
+ name=name,
133
+ email=email,
134
+ role=role
135
+ )
136
+ user.set_password(password)
137
+
138
+ db.session.add(user)
139
+ db.session.commit()
140
+
141
+ logger.info(f"User registered: {matric_no} ({role})")
142
+
143
+ return format_success_response(
144
+ data=user.to_dict(),
145
+ message=f"{role.capitalize()} account created successfully!"
146
+ )
147
+
148
+ except Exception as e:
149
+ db.session.rollback()
150
+ logger.error(f"User registration error: {str(e)}")
151
+ return format_error_response(f"Registration failed: {str(e)}", 500)
152
+
153
+
154
+ @app.route('/api/auth/login', methods=['POST'])
155
+ def login():
156
+ """Login user and create session"""
157
+ try:
158
+ data = request.get_json()
159
+
160
+ if not all(k in data for k in ['matric_no', 'password']):
161
+ return format_error_response("Missing required fields: matric_no, password")
162
+
163
+ matric_no = data['matric_no'].upper().strip()
164
+ password = data['password']
165
+
166
+ # Find user
167
+ user = User.query.filter_by(matric_no=matric_no).first()
168
+
169
+ if not user or not user.check_password(password):
170
+ return format_error_response("Invalid matric number or password", 401)
171
+
172
+ if not user.is_active:
173
+ return format_error_response("Account is inactive. Please contact admin.", 403)
174
+
175
+ # Create session
176
+ session['user_id'] = user.id
177
+ session['matric_no'] = user.matric_no
178
+ session['role'] = user.role
179
+ session['name'] = user.name
180
+
181
+ logger.info(f"User logged in: {matric_no} ({user.role})")
182
+
183
+ return format_success_response(
184
+ data={
185
+ 'user': user.to_dict(),
186
+ 'redirect': '/student' if user.role == 'student' else '/lecturer'
187
+ },
188
+ message=f"Welcome back, {user.name}!"
189
+ )
190
+
191
+ except Exception as e:
192
+ logger.error(f"Login error: {str(e)}")
193
+ return format_error_response(f"Login failed: {str(e)}", 500)
194
+
195
+
196
+ @app.route('/api/auth/logout', methods=['POST'])
197
+ @login_required
198
+ def logout():
199
+ """Logout user and destroy session"""
200
+ try:
201
+ user_name = session.get('name', 'User')
202
+ matric_no = session.get('matric_no', 'Unknown')
203
+
204
+ # Clear all session data
205
+ session.clear()
206
+
207
+ logger.info(f"User logged out: {matric_no}")
208
+
209
+ return format_success_response(
210
+ message=f"Goodbye, {user_name}! You have been logged out successfully."
211
+ )
212
+
213
+ except Exception as e:
214
+ logger.error(f"Logout error: {str(e)}")
215
+ return format_error_response(f"Logout failed: {str(e)}", 500)
216
+
217
+
218
+ @app.route('/api/auth/current_user', methods=['GET'])
219
+ def get_current_user_api():
220
+ """Get current logged in user"""
221
+ user = get_current_user()
222
+ if user:
223
+ return format_success_response(data=user.to_dict())
224
+ return format_error_response("Not logged in", 401)
225
+
226
+
227
+ # ==================== API ENDPOINTS ====================
228
+
229
+ @app.route('/api/register_face', methods=['POST'])
230
+ def register_face():
231
+ """
232
+ Register student face encoding
233
+ Implements: PB-1 (Register Student Face Data)
234
+ """
235
+ try:
236
+ data = request.get_json()
237
+
238
+ # Validate input
239
+ if not all(k in data for k in ['student_id', 'name', 'image']):
240
+ return format_error_response("Missing required fields: student_id, name, image")
241
+
242
+ student_id = data['student_id']
243
+ name = data['name']
244
+ email = data.get('email')
245
+ image_base64 = data['image']
246
+
247
+ # Check if student already exists
248
+ existing_student = Student.query.filter_by(student_id=student_id).first()
249
+ if existing_student:
250
+ return format_error_response("Student ID already registered", 409)
251
+
252
+ # Get user account (if exists)
253
+ user = User.query.filter_by(matric_no=student_id).first()
254
+
255
+ # Convert base64 to image
256
+ try:
257
+ image = base64_to_image(image_base64)
258
+ image_array = image_to_numpy(image)
259
+ except Exception as e:
260
+ return format_error_response(f"Invalid image data: {str(e)}")
261
+
262
+ # Generate face encoding
263
+ success, result, face_location = face_recognition_module.register_face(image_array)
264
+
265
+ if not success:
266
+ return format_error_response(result)
267
+
268
+ # Create student record
269
+ student = Student(
270
+ student_id=student_id,
271
+ name=name,
272
+ email=email,
273
+ user_id=user.id if user else None # Link to user account if exists
274
+ )
275
+ student.set_face_encoding(result)
276
+
277
+ db.session.add(student)
278
+ db.session.commit()
279
+
280
+ logger.info(f"Student registered: {student_id} - {name}")
281
+
282
+ return format_success_response(
283
+ data=student.to_dict(),
284
+ message="Face registered successfully!"
285
+ )
286
+
287
+ except Exception as e:
288
+ db.session.rollback()
289
+ logger.error(f"Registration error: {str(e)}")
290
+ return format_error_response(f"Registration failed: {str(e)}", 500)
291
+
292
+
293
+ @app.route('/api/verify_face', methods=['POST'])
294
+ def verify_face():
295
+ """
296
+ Verify student face against stored encoding
297
+ Implements: PB-3 (Face Recognition Matching)
298
+ Implements KR Rule #1: ∀x ((CapturedFace(x) ∧ MatchStored(x)) → FaceMatch(x))
299
+ """
300
+ try:
301
+ data = request.get_json()
302
+
303
+ if not all(k in data for k in ['student_id', 'image']):
304
+ return format_error_response("Missing required fields: student_id, image")
305
+
306
+ student_id = data['student_id']
307
+ image_base64 = data['image']
308
+
309
+ # Get student record
310
+ student = Student.query.filter_by(student_id=student_id).first()
311
+ if not student:
312
+ return format_error_response("Student not found. Please register first.", 404)
313
+
314
+ if not student.is_active:
315
+ return format_error_response("Student account is inactive", 403)
316
+
317
+ # Get stored encoding
318
+ stored_encoding = student.get_face_encoding()
319
+ if not stored_encoding:
320
+ return format_error_response("No face encoding found. Please register your face.", 404)
321
+
322
+ # Convert base64 to image
323
+ try:
324
+ image = base64_to_image(image_base64)
325
+ image_array = image_to_numpy(image)
326
+ except Exception as e:
327
+ return format_error_response(f"Invalid image data: {str(e)}")
328
+
329
+ # Generate encoding from captured image
330
+ success, result = face_recognition_module.generate_face_encoding(image_array)
331
+
332
+ if not success:
333
+ return format_error_response(result)
334
+
335
+ captured_encoding = result
336
+
337
+ # Verify face match
338
+ is_match, confidence = face_recognition_module.verify_face(captured_encoding, stored_encoding)
339
+
340
+ if is_match:
341
+ logger.info(f"Face verified for student {student_id}: confidence={confidence:.2f}%")
342
+ return format_success_response(
343
+ data={
344
+ 'verified': True,
345
+ 'confidence': round(confidence, 2),
346
+ 'student': student.to_dict()
347
+ },
348
+ message=f"Face verified! Confidence: {confidence:.1f}%"
349
+ )
350
+ else:
351
+ logger.warning(f"Face verification failed for {student_id}: confidence={confidence:.2f}%")
352
+ return format_error_response(
353
+ f"Face verification failed. Confidence too low: {confidence:.1f}%",
354
+ 401
355
+ )
356
+
357
+ except Exception as e:
358
+ logger.error(f"Face verification error: {str(e)}")
359
+ return format_error_response(f"Verification failed: {str(e)}", 500)
360
+
361
+
362
+ @app.route('/api/verify_location', methods=['POST'])
363
+ def verify_location():
364
+ """
365
+ Verify student location against classroom boundaries
366
+ Implements: PB-5 (Geofencing Logic – Classroom Detection)
367
+ Implements KR Rule #2: ∀x[(Student(x) ∧ IsWithinAllowedArea(x)) → VerifiedLocation(x)]
368
+ """
369
+ try:
370
+ data = request.get_json()
371
+
372
+ if not all(k in data for k in ['latitude', 'longitude', 'session_id']):
373
+ return format_error_response("Missing required fields: latitude, longitude, session_id")
374
+
375
+ latitude = data['latitude']
376
+ longitude = data['longitude']
377
+ session_id = data['session_id']
378
+
379
+ # Get session
380
+ session_obj = AttendanceSession.query.get(session_id)
381
+ if not session_obj:
382
+ return format_error_response("Session not found", 404)
383
+
384
+ if not session_obj.is_active:
385
+ return format_error_response("Session is not active", 403)
386
+
387
+ # Verify location
388
+ result = geolocation_module.verify_location(
389
+ latitude,
390
+ longitude,
391
+ session_obj.classroom_id
392
+ )
393
+
394
+ if result['verified']:
395
+ logger.info(f"Location verified: distance={result['distance']:.2f}m")
396
+ return format_success_response(
397
+ data={
398
+ 'verified': True,
399
+ 'distance': round(result['distance'], 2),
400
+ 'classroom': session_obj.classroom.to_dict()
401
+ },
402
+ message=result['message']
403
+ )
404
+ else:
405
+ logger.warning(f"Location verification failed: {result['error']}")
406
+ return format_error_response(result['error'] or result['message'], 403)
407
+
408
+ except Exception as e:
409
+ logger.error(f"Location verification error: {str(e)}")
410
+ return format_error_response(f"Location verification failed: {str(e)}", 500)
411
+
412
+
413
+ @app.route('/api/mark_attendance', methods=['POST'])
414
+ def mark_attendance():
415
+ """
416
+ Mark student attendance after all verifications
417
+ Implements: PB-12 (Mark Attendance in Database)
418
+ Implements KR Rule #4: ∀x ((FaceMatch(x) ∧ LocationValid(x)) → GrantCodeAccess(x))
419
+ Implements KR Rule #5: ∀x ((ValidCodeEntry(User,x) ∧ WithinCycle(Code,x)) → MarkPresent(System,x))
420
+ """
421
+ try:
422
+ data = request.get_json()
423
+
424
+ required_fields = ['student_id', 'session_id', 'code', 'latitude', 'longitude']
425
+ if not all(k in data for k in required_fields):
426
+ return format_error_response(f"Missing required fields: {', '.join(required_fields)}")
427
+
428
+ student_id = data['student_id']
429
+ session_id = data['session_id']
430
+ code = data['code']
431
+ latitude = data['latitude']
432
+ longitude = data['longitude']
433
+
434
+ # Get student
435
+ student = Student.query.filter_by(student_id=student_id).first()
436
+ if not student:
437
+ return format_error_response("Student not found", 404)
438
+
439
+ # Get session
440
+ session_obj = AttendanceSession.query.get(session_id)
441
+ if not session_obj:
442
+ return format_error_response("Session not found", 404)
443
+
444
+ if not session_obj.is_active:
445
+ return format_error_response("Session is not active", 403)
446
+
447
+ # Check if already marked
448
+ existing_record = AttendanceRecord.query.filter_by(
449
+ session_id=session_id,
450
+ student_id=student.id
451
+ ).first()
452
+
453
+ if existing_record:
454
+ return format_error_response("Attendance already marked for this session", 409)
455
+
456
+ # Validate code
457
+ is_valid, message = attendance_code_module.is_code_valid(session_id, code)
458
+
459
+ if not is_valid:
460
+ return format_error_response(message, 401)
461
+
462
+ # Verify location one more time
463
+ location_result = geolocation_module.verify_location(
464
+ latitude,
465
+ longitude,
466
+ session_obj.classroom_id
467
+ )
468
+
469
+ if not location_result['verified']:
470
+ return format_error_response(location_result['error'] or "Location verification failed", 403)
471
+
472
+ # Create attendance record
473
+ attendance = AttendanceRecord(
474
+ session_id=session_id,
475
+ student_id=student.id,
476
+ face_verified=True, # Assumed verified before reaching this endpoint
477
+ location_verified=True,
478
+ code_verified=True,
479
+ student_latitude=float(latitude),
480
+ student_longitude=float(longitude),
481
+ distance_from_classroom=location_result['distance'],
482
+ status='present'
483
+ )
484
+
485
+ db.session.add(attendance)
486
+ db.session.commit()
487
+
488
+ logger.info(f"Attendance marked: {student.name} for session {session_id}")
489
+
490
+ return format_success_response(
491
+ data=attendance.to_dict(),
492
+ message="Attendance marked successfully! ✓"
493
+ )
494
+
495
+ except Exception as e:
496
+ db.session.rollback()
497
+ logger.error(f"Mark attendance error: {str(e)}")
498
+ return format_error_response(f"Failed to mark attendance: {str(e)}", 500)
499
+
500
+
501
+ @app.route('/api/create_session', methods=['POST'])
502
+ def create_session():
503
+ """
504
+ Create new attendance session
505
+ Implements: US-06 (Generate Auto-Refresh Codes)
506
+ """
507
+ try:
508
+ data = request.get_json()
509
+
510
+ if not all(k in data for k in ['course_name', 'classroom_id', 'lecturer_name']):
511
+ return format_error_response("Missing required fields: course_name, classroom_id, lecturer_name")
512
+
513
+ course_name = data['course_name']
514
+ classroom_id = data['classroom_id']
515
+ lecturer_name = data['lecturer_name']
516
+
517
+ # Verify classroom exists
518
+ classroom = Classroom.query.get(classroom_id)
519
+ if not classroom:
520
+ return format_error_response("Classroom not found", 404)
521
+
522
+ # Create session
523
+ session_obj = AttendanceSession(
524
+ course_name=course_name,
525
+ classroom_id=classroom_id,
526
+ lecturer_name=lecturer_name,
527
+ is_active=True
528
+ )
529
+
530
+ db.session.add(session_obj)
531
+ db.session.commit()
532
+
533
+ # Generate initial code
534
+ success, code = attendance_code_module.create_session_code(session_obj.id)
535
+
536
+ if not success:
537
+ return format_error_response(f"Session created but code generation failed: {code}")
538
+
539
+ logger.info(f"Session created: {course_name} by {lecturer_name}")
540
+
541
+ return format_success_response(
542
+ data=session_obj.to_dict(),
543
+ message="Session created successfully!"
544
+ )
545
+
546
+ except Exception as e:
547
+ db.session.rollback()
548
+ logger.error(f"Create session error: {str(e)}")
549
+ return format_error_response(f"Failed to create session: {str(e)}", 500)
550
+
551
+
552
+ @app.route('/api/get_session/<int:session_id>', methods=['GET'])
553
+ def get_session(session_id):
554
+ """Get session details including current code"""
555
+ try:
556
+ session_obj = AttendanceSession.query.get(session_id)
557
+
558
+ if not session_obj:
559
+ return format_error_response("Session not found", 404)
560
+
561
+ # Get code status
562
+ code_status = attendance_code_module.get_code_status(session_id)
563
+
564
+ # Auto-refresh if needed
565
+ if code_status.get('needs_refresh'):
566
+ attendance_code_module.auto_refresh_code(session_id)
567
+ code_status = attendance_code_module.get_code_status(session_id)
568
+
569
+ session_data = session_obj.to_dict()
570
+ session_data['code_status'] = code_status
571
+
572
+ return format_success_response(data=session_data)
573
+
574
+ except Exception as e:
575
+ logger.error(f"Get session error: {str(e)}")
576
+ return format_error_response(f"Failed to get session: {str(e)}", 500)
577
+
578
+
579
+ @app.route('/api/get_active_sessions', methods=['GET'])
580
+ def get_active_sessions():
581
+ """Get all active sessions"""
582
+ try:
583
+ sessions = AttendanceSession.query.filter_by(is_active=True).all()
584
+ return format_success_response(
585
+ data=[session.to_dict() for session in sessions]
586
+ )
587
+ except Exception as e:
588
+ logger.error(f"Get active sessions error: {str(e)}")
589
+ return format_error_response(f"Failed to get sessions: {str(e)}", 500)
590
+
591
+
592
+ @app.route('/api/get_attendance/<int:session_id>', methods=['GET'])
593
+ def get_attendance(session_id):
594
+ """
595
+ Get attendance records for a session
596
+ Implements: US-07 (View Real-time Attendance)
597
+ """
598
+ try:
599
+ session_obj = AttendanceSession.query.get(session_id)
600
+
601
+ if not session_obj:
602
+ return format_error_response("Session not found", 404)
603
+
604
+ records = AttendanceRecord.query.filter_by(session_id=session_id).all()
605
+
606
+ return format_success_response(
607
+ data={
608
+ 'session': session_obj.to_dict(),
609
+ 'records': [record.to_dict() for record in records],
610
+ 'total_present': len(records)
611
+ }
612
+ )
613
+
614
+ except Exception as e:
615
+ logger.error(f"Get attendance error: {str(e)}")
616
+ return format_error_response(f"Failed to get attendance: {str(e)}", 500)
617
+
618
+
619
+ @app.route('/api/end_session/<int:session_id>', methods=['POST'])
620
+ def end_session(session_id):
621
+ """End an attendance session"""
622
+ try:
623
+ session_obj = AttendanceSession.query.get(session_id)
624
+
625
+ if not session_obj:
626
+ return format_error_response("Session not found", 404)
627
+
628
+ session_obj.is_active = False
629
+ session_obj.end_time = datetime.utcnow()
630
+
631
+ db.session.commit()
632
+
633
+ logger.info(f"Session ended: {session_id}")
634
+
635
+ return format_success_response(message="Session ended successfully")
636
+
637
+ except Exception as e:
638
+ db.session.rollback()
639
+ logger.error(f"End session error: {str(e)}")
640
+ return format_error_response(f"Failed to end session: {str(e)}", 500)
641
+
642
+
643
+ @app.route('/api/student/history', methods=['GET'])
644
+ @login_required
645
+ @role_required('student')
646
+ def get_student_history():
647
+ """Get attendance history for logged-in student"""
648
+ try:
649
+ # Get current user's student profile
650
+ current_user = get_current_user()
651
+ student = Student.query.filter_by(student_id=current_user.matric_no).first()
652
+
653
+ if not student:
654
+ return format_error_response("Student profile not found", 404)
655
+
656
+ # Get all attendance records for this student
657
+ records = AttendanceRecord.query.filter_by(student_id=student.id)\
658
+ .order_by(AttendanceRecord.marked_at.desc()).all()
659
+
660
+ # Format response with session details
661
+ history = []
662
+ for record in records:
663
+ session = record.session
664
+ history.append({
665
+ 'id': record.id,
666
+ 'course_name': session.course_name,
667
+ 'lecturer_name': session.lecturer_name,
668
+ 'classroom': session.classroom.name,
669
+ 'building': session.classroom.building,
670
+ 'status': record.status,
671
+ 'marked_at': record.marked_at.isoformat() if record.marked_at else None,
672
+ 'session_date': session.start_time.isoformat() if session.start_time else None,
673
+ 'face_verified': record.face_verified,
674
+ 'location_verified': record.location_verified,
675
+ 'code_verified': record.code_verified,
676
+ 'distance_from_classroom': round(record.distance_from_classroom, 2) if record.distance_from_classroom else None
677
+ })
678
+
679
+ return format_success_response(
680
+ data={
681
+ 'student': student.to_dict(),
682
+ 'total_records': len(history),
683
+ 'history': history
684
+ }
685
+ )
686
+
687
+ except Exception as e:
688
+ logger.error(f"Get student history error: {str(e)}")
689
+ return format_error_response(f"Failed to get history: {str(e)}", 500)
690
+
691
+
692
+ @app.route('/api/lecturer/history', methods=['GET'])
693
+ @login_required
694
+ @role_required('lecturer')
695
+ def get_lecturer_history():
696
+ """Get session history for logged-in lecturer"""
697
+ try:
698
+ current_user = get_current_user()
699
+
700
+ # Get all sessions created by this lecturer (match by name)
701
+ sessions = AttendanceSession.query.filter_by(lecturer_name=current_user.name)\
702
+ .order_by(AttendanceSession.start_time.desc()).all()
703
+
704
+ # Format response with attendance statistics
705
+ history = []
706
+ for session in sessions:
707
+ # Count attendance records
708
+ total_attendance = len(session.attendance_records)
709
+ present_count = sum(1 for r in session.attendance_records if r.status == 'present')
710
+
711
+ # Calculate session duration
712
+ if session.end_time and session.start_time:
713
+ duration_minutes = int((session.end_time - session.start_time).total_seconds() / 60)
714
+ elif session.is_active and session.start_time:
715
+ duration_minutes = int((datetime.utcnow() - session.start_time).total_seconds() / 60)
716
+ else:
717
+ duration_minutes = 0
718
+
719
+ history.append({
720
+ 'id': session.id,
721
+ 'course_name': session.course_name,
722
+ 'lecturer_name': session.lecturer_name,
723
+ 'classroom': session.classroom.name,
724
+ 'building': session.classroom.building,
725
+ 'start_time': session.start_time.isoformat() if session.start_time else None,
726
+ 'end_time': session.end_time.isoformat() if session.end_time else None,
727
+ 'is_active': session.is_active,
728
+ 'duration_minutes': duration_minutes,
729
+ 'total_attendance': total_attendance,
730
+ 'present_count': present_count,
731
+ 'attendance_records': [
732
+ {
733
+ 'student_id': r.student.student_id if r.student else 'Unknown',
734
+ 'student_name': r.student.name if r.student else 'Unknown',
735
+ 'status': r.status,
736
+ 'marked_at': r.marked_at.isoformat() if r.marked_at else None,
737
+ 'face_verified': r.face_verified,
738
+ 'location_verified': r.location_verified,
739
+ 'code_verified': r.code_verified
740
+ } for r in session.attendance_records
741
+ ]
742
+ })
743
+
744
+ return format_success_response(
745
+ data={
746
+ 'lecturer': {
747
+ 'name': current_user.name,
748
+ 'matric_no': current_user.matric_no
749
+ },
750
+ 'total_sessions': len(history),
751
+ 'history': history
752
+ }
753
+ )
754
+
755
+ except Exception as e:
756
+ logger.error(f"Get lecturer history error: {str(e)}")
757
+ return format_error_response(f"Failed to get history: {str(e)}", 500)
758
+
759
+
760
+ @app.route('/api/classrooms', methods=['GET'])
761
+ def get_classrooms():
762
+ """Get all classrooms"""
763
+ try:
764
+ classrooms = geolocation_module.get_all_classrooms()
765
+ return format_success_response(data=classrooms)
766
+ except Exception as e:
767
+ logger.error(f"Get classrooms error: {str(e)}")
768
+ return format_error_response(f"Failed to get classrooms: {str(e)}", 500)
769
+
770
+
771
+ # ==================== ERROR HANDLERS ====================
772
+
773
+ @app.errorhandler(404)
774
+ def not_found(error):
775
+ return format_error_response("Resource not found", 404)
776
+
777
+
778
+ @app.errorhandler(500)
779
+ def internal_error(error):
780
+ db.session.rollback()
781
+ return format_error_response("Internal server error", 500)
782
+
783
+
784
+ # ==================== MAIN ====================
785
+
786
+ if __name__ == '__main__':
787
+ with app.app_context():
788
+ db.create_all()
789
+ logger.info("Database tables created")
790
+
791
+ app.run(debug=True, host='0.0.0.0', port=5000)
attendance_code_module.py ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Attendance Code Module for Attendr
3
+ Implements KR Rule #5: Auto-Refresh Code Confirmation
4
+ ∀x ((ValidCodeEntry(User,x) ∧ WithinCycle(Code,x)) → MarkPresent(System,x))
5
+ """
6
+
7
+ import random
8
+ import string
9
+ from datetime import datetime, timedelta
10
+ from models import AttendanceSession, db
11
+ from config import Config
12
+ import logging
13
+
14
+ # Set up logging
15
+ logging.basicConfig(level=logging.INFO)
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class AttendanceCodeModule:
20
+ """Handles attendance code generation, validation, and auto-refresh"""
21
+
22
+ def __init__(self, code_length=None, refresh_interval=None):
23
+ """
24
+ Initialize attendance code module
25
+
26
+ Args:
27
+ code_length: Length of the code (default from config)
28
+ refresh_interval: Code refresh interval in seconds (default from config)
29
+ """
30
+ self.code_length = code_length or Config.CODE_LENGTH
31
+ self.refresh_interval = refresh_interval or Config.CODE_REFRESH_INTERVAL
32
+ logger.info(f"Attendance Code Module initialized: length={self.code_length}, refresh={self.refresh_interval}s")
33
+
34
+ def generate_code(self):
35
+ """
36
+ Generate a random attendance code
37
+
38
+ Returns:
39
+ String code (e.g., "A3X9K2")
40
+ """
41
+ # Use uppercase letters and digits for clarity
42
+ characters = string.ascii_uppercase + string.digits
43
+ code = ''.join(random.choice(characters) for _ in range(self.code_length))
44
+ logger.info(f"Generated new code: {code}")
45
+ return code
46
+
47
+ def create_session_code(self, session_id):
48
+ """
49
+ Create and store a new code for a session
50
+
51
+ Args:
52
+ session_id: ID of the attendance session
53
+
54
+ Returns:
55
+ tuple: (success, code or error_message)
56
+ """
57
+ try:
58
+ session = AttendanceSession.query.get(session_id)
59
+
60
+ if not session:
61
+ return False, "Session not found"
62
+
63
+ if not session.is_active:
64
+ return False, "Session is not active"
65
+
66
+ # Generate new code
67
+ new_code = self.generate_code()
68
+
69
+ # Update session
70
+ session.current_code = new_code
71
+ session.code_generated_at = datetime.utcnow()
72
+
73
+ db.session.commit()
74
+
75
+ logger.info(f"Session {session_id} code updated to: {new_code}")
76
+ return True, new_code
77
+
78
+ except Exception as e:
79
+ db.session.rollback()
80
+ logger.error(f"Error creating session code: {str(e)}")
81
+ return False, f"Failed to create code: {str(e)}"
82
+
83
+ def is_code_valid(self, session_id, entered_code):
84
+ """
85
+ Validate if entered code matches current session code and is within refresh cycle
86
+ Implements: ValidCodeEntry(User,x) ∧ WithinCycle(Code,x)
87
+
88
+ Args:
89
+ session_id: ID of the attendance session
90
+ entered_code: Code entered by student
91
+
92
+ Returns:
93
+ tuple: (is_valid: bool, message: str)
94
+ """
95
+ try:
96
+ session = AttendanceSession.query.get(session_id)
97
+
98
+ if not session:
99
+ return False, "Session not found"
100
+
101
+ if not session.is_active:
102
+ return False, "Session is no longer active"
103
+
104
+ if not session.current_code:
105
+ return False, "No active code for this session"
106
+
107
+ # Check if code matches (case-insensitive)
108
+ if session.current_code.upper() != entered_code.upper():
109
+ logger.warning(f"Code mismatch: expected {session.current_code}, got {entered_code}")
110
+ return False, "Invalid code. Please check and try again."
111
+
112
+ # Check if code is within refresh cycle
113
+ if not session.code_generated_at:
114
+ return False, "Code timestamp not found"
115
+
116
+ time_elapsed = (datetime.utcnow() - session.code_generated_at).total_seconds()
117
+
118
+ if time_elapsed > self.refresh_interval:
119
+ logger.warning(f"Code expired: {time_elapsed:.0f}s elapsed (max: {self.refresh_interval}s)")
120
+ return False, f"Code has expired. Please use the current code displayed by your lecturer."
121
+
122
+ # Code is valid
123
+ time_remaining = self.refresh_interval - time_elapsed
124
+ logger.info(f"Code validated successfully. Time remaining: {time_remaining:.0f}s")
125
+ return True, "Code verified successfully"
126
+
127
+ except Exception as e:
128
+ logger.error(f"Code validation error: {str(e)}")
129
+ return False, f"Validation failed: {str(e)}"
130
+
131
+ def should_refresh_code(self, session_id):
132
+ """
133
+ Check if code should be refreshed based on time elapsed
134
+
135
+ Args:
136
+ session_id: ID of the attendance session
137
+
138
+ Returns:
139
+ bool: True if code should be refreshed
140
+ """
141
+ try:
142
+ session = AttendanceSession.query.get(session_id)
143
+
144
+ if not session or not session.is_active:
145
+ return False
146
+
147
+ if not session.code_generated_at:
148
+ return True # No code generated yet
149
+
150
+ time_elapsed = (datetime.utcnow() - session.code_generated_at).total_seconds()
151
+
152
+ return time_elapsed >= self.refresh_interval
153
+
154
+ except Exception as e:
155
+ logger.error(f"Error checking refresh status: {str(e)}")
156
+ return False
157
+
158
+ def auto_refresh_code(self, session_id):
159
+ """
160
+ Automatically refresh code if needed
161
+
162
+ Args:
163
+ session_id: ID of the attendance session
164
+
165
+ Returns:
166
+ tuple: (refreshed: bool, code or None)
167
+ """
168
+ try:
169
+ if self.should_refresh_code(session_id):
170
+ success, code = self.create_session_code(session_id)
171
+ if success:
172
+ logger.info(f"Code auto-refreshed for session {session_id}")
173
+ return True, code
174
+ else:
175
+ return False, None
176
+ else:
177
+ # Return current code
178
+ session = AttendanceSession.query.get(session_id)
179
+ if session and session.current_code:
180
+ return False, session.current_code
181
+ return False, None
182
+
183
+ except Exception as e:
184
+ logger.error(f"Auto-refresh error: {str(e)}")
185
+ return False, None
186
+
187
+ def get_code_status(self, session_id):
188
+ """
189
+ Get current code status including time remaining
190
+
191
+ Args:
192
+ session_id: ID of the attendance session
193
+
194
+ Returns:
195
+ dict: Code status information
196
+ """
197
+ try:
198
+ session = AttendanceSession.query.get(session_id)
199
+
200
+ if not session:
201
+ return {'error': 'Session not found'}
202
+
203
+ if not session.is_active:
204
+ return {'error': 'Session is not active'}
205
+
206
+ if not session.current_code or not session.code_generated_at:
207
+ return {
208
+ 'code': None,
209
+ 'time_remaining': 0,
210
+ 'needs_refresh': True
211
+ }
212
+
213
+ time_elapsed = (datetime.utcnow() - session.code_generated_at).total_seconds()
214
+ time_remaining = max(0, self.refresh_interval - time_elapsed)
215
+ needs_refresh = time_elapsed >= self.refresh_interval
216
+
217
+ return {
218
+ 'code': session.current_code,
219
+ 'time_remaining': int(time_remaining),
220
+ 'time_elapsed': int(time_elapsed),
221
+ 'refresh_interval': self.refresh_interval,
222
+ 'needs_refresh': needs_refresh,
223
+ 'generated_at': session.code_generated_at.isoformat()
224
+ }
225
+
226
+ except Exception as e:
227
+ logger.error(f"Error getting code status: {str(e)}")
228
+ return {'error': str(e)}
229
+
230
+
231
+ # Singleton instance
232
+ attendance_code_module = AttendanceCodeModule()
attendr.db ADDED
Binary file (45.1 kB). View file
 
attendr.sql ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ CREATE DATABASE IF NOT EXISTS attendr;
2
+ USE attendr;
3
+
4
+ START TRANSACTION;
5
+ CREATE TABLE attendance_records (
6
+ id INTEGER NOT NULL,
7
+ session_id INTEGER NOT NULL,
8
+ student_id INTEGER NOT NULL,
9
+ marked_at DATETIME,
10
+ face_verified BOOLEAN,
11
+ location_verified BOOLEAN,
12
+ code_verified BOOLEAN,
13
+ student_latitude FLOAT,
14
+ student_longitude FLOAT,
15
+ distance_from_classroom FLOAT,
16
+ status VARCHAR(20),
17
+ PRIMARY KEY (id),
18
+ FOREIGN KEY(session_id) REFERENCES attendance_sessions (id),
19
+ FOREIGN KEY(student_id) REFERENCES students (id)
20
+ );
21
+ INSERT INTO `attendance_records` VALUES(1,2,1,'2025-11-22 16:39:17.926194',1,1,1,3.1222562,101.7636743,6.719226959803195314e+02,'present');
22
+ INSERT INTO `attendance_records` VALUES(2,3,1,'2025-11-22 16:46:07.075823',1,1,1,3.1222616,101.7636711,6.714557167968994236e+02,'present');
23
+ INSERT INTO `attendance_records` VALUES(3,6,1,'2025-11-22 17:12:17.627831',1,1,1,3.122261,101.7636698,6.715625427798626106e+02,'present');
24
+ INSERT INTO `attendance_records` VALUES(4,7,1,'2025-11-22 15:50:16.138101',1,1,1,0.0,0.0,10.0,'present');
25
+ INSERT INTO `attendance_records` VALUES(5,8,1,'2025-11-22 17:31:19.262173',1,1,1,3.1222586,101.7636816,6.714266242365291645e+02,'present');
26
+ CREATE TABLE attendance_sessions (
27
+ id INTEGER NOT NULL,
28
+ course_name VARCHAR(100) NOT NULL,
29
+ classroom_id INTEGER NOT NULL,
30
+ lecturer_name VARCHAR(100) NOT NULL,
31
+ start_time DATETIME,
32
+ end_time DATETIME,
33
+ is_active BOOLEAN,
34
+ current_code VARCHAR(10),
35
+ code_generated_at DATETIME,
36
+ PRIMARY KEY (id),
37
+ FOREIGN KEY(classroom_id) REFERENCES classrooms (id)
38
+ );
39
+ INSERT INTO `attendance_sessions` VALUES(1,'AI',46,'ZHANG LING HE','2025-11-22 16:34:53.967623','2025-11-22 16:35:22.483428',0,'EAB1FQ','2025-11-22 16:34:53.977748');
40
+ INSERT INTO `attendance_sessions` VALUES(2,'AI',46,'ZHANG LING HE','2025-11-22 16:35:34.223978','2025-11-22 16:45:32.132931',0,'87JFTB','2025-11-22 16:45:23.271856');
41
+ INSERT INTO `attendance_sessions` VALUES(3,'AD',46,'ZHANG LING HE','2025-11-22 16:45:46.604739','2025-11-22 16:50:30.716018',0,'RJII20','2025-11-22 16:50:22.204453');
42
+ INSERT INTO `attendance_sessions` VALUES(4,'SECR3104 - Applications Development',46,'ZHANG LING HE','2025-11-22 17:04:10.912458','2025-11-22 17:04:36.114555',0,'86SONH','2025-11-22 17:04:32.116897');
43
+ INSERT INTO `attendance_sessions` VALUES(5,'SECR3104 - Applications Development',46,'ZHANG LING HE','2025-11-22 17:04:54.989793','2025-11-22 17:09:42.210274',0,'L8D01H','2025-11-22 17:09:33.982659');
44
+ INSERT INTO `attendance_sessions` VALUES(6,'SECJ3553 - Artificial Intelligence',46,'ZHANG LING HE','2025-11-22 17:12:01.124969','2025-11-22 17:26:36.046635',0,'446MC7','2025-11-22 17:26:33.821599');
45
+ INSERT INTO `attendance_sessions` VALUES(7,'TEST101 - Testing',1,'Test Lecturer','2025-11-22 15:20:16.131518','2025-11-22 16:20:16.131518',0,NULL,NULL);
46
+ INSERT INTO `attendance_sessions` VALUES(8,'SECR1013 - Digital Logic',46,'ZHANG LING HE','2025-11-22 17:30:15.307311','2025-11-22 17:31:23.154043',0,'D5DYN5','2025-11-22 17:31:15.654521');
47
+ CREATE TABLE classrooms (
48
+ id INTEGER NOT NULL,
49
+ name VARCHAR(100) NOT NULL,
50
+ building VARCHAR(100),
51
+ latitude FLOAT NOT NULL,
52
+ longitude FLOAT NOT NULL,
53
+ radius_meters INTEGER,
54
+ PRIMARY KEY (id)
55
+ );
56
+ INSERT INTO `classrooms` VALUES(1,'N28-105-01','N28 - Faculty of Computing',1.5638,103.6388,50);
57
+ INSERT INTO `classrooms` VALUES(2,'N28-116-02','N28 - Faculty of Computing',1.5639,103.6388,50);
58
+ INSERT INTO `classrooms` VALUES(3,'N28-112-02','N28 - Faculty of Computing',1.564,103.6388,50);
59
+ INSERT INTO `classrooms` VALUES(4,'N28-111-01','N28 - Faculty of Computing',1.5641,103.6388,50);
60
+ INSERT INTO `classrooms` VALUES(5,'N28-106-01','N28 - Faculty of Computing',1.5642,103.6388,50);
61
+ INSERT INTO `classrooms` VALUES(6,'N28-107-01','N28 - Faculty of Computing',1.5643,103.6388,50);
62
+ INSERT INTO `classrooms` VALUES(7,'N28-108-01','N28 - Faculty of Computing',1.5644,103.6388,50);
63
+ INSERT INTO `classrooms` VALUES(8,'N28-351-02','N28 - Level 3',1.5645,103.6388,50);
64
+ INSERT INTO `classrooms` VALUES(9,'N28-301-01','N28 - Level 3',1.5646,103.6388,50);
65
+ INSERT INTO `classrooms` VALUES(10,'N28-301-01B','N28 - Level 3',1.5647,103.6388,50);
66
+ INSERT INTO `classrooms` VALUES(11,'N28A-BT1','N28A - Tutorial Block',1.5638,103.6389,50);
67
+ INSERT INTO `classrooms` VALUES(12,'N28A-BT2','N28A - Tutorial Block',1.5639,103.6389,50);
68
+ INSERT INTO `classrooms` VALUES(13,'N28A-BT3','N28A - Tutorial Block',1.564,103.6389,50);
69
+ INSERT INTO `classrooms` VALUES(14,'N28A-BT4','N28A - Tutorial Block',1.5641,103.6389,50);
70
+ INSERT INTO `classrooms` VALUES(15,'N28A-BK1','N28A - Tutorial Block',1.5642,103.6389,50);
71
+ INSERT INTO `classrooms` VALUES(16,'N28A-BK2','N28A - Tutorial Block',1.5643,103.6389,50);
72
+ INSERT INTO `classrooms` VALUES(17,'N28A-BK5','N28A - Tutorial Block',1.5644,103.6389,50);
73
+ INSERT INTO `classrooms` VALUES(18,'N28A-BK6','N28A - Tutorial Block',1.5645,103.6389,50);
74
+ INSERT INTO `classrooms` VALUES(19,'N28-502-01','N28 - Level 5',1.5646,103.6389,50);
75
+ INSERT INTO `classrooms` VALUES(20,'N28-505-01','N28 - Level 5',1.5647,103.6389,50);
76
+ INSERT INTO `classrooms` VALUES(21,'N28-506-01','N28 - Level 5',1.5638,1.036390000000000101e+02,50);
77
+ INSERT INTO `classrooms` VALUES(22,'N28-512-01','N28 - Level 5',1.5639,1.036390000000000101e+02,50);
78
+ INSERT INTO `classrooms` VALUES(23,'N28-516-01','N28 - Level 5',1.564,1.036390000000000101e+02,50);
79
+ INSERT INTO `classrooms` VALUES(24,'N28-517-02','N28 - Level 5',1.5641,1.036390000000000101e+02,50);
80
+ INSERT INTO `classrooms` VALUES(25,'N28-518-02','N28 - Level 5',1.5642,1.036390000000000101e+02,50);
81
+ INSERT INTO `classrooms` VALUES(26,'N28-524-02','N28 - Level 5',1.5643,1.036390000000000101e+02,50);
82
+ INSERT INTO `classrooms` VALUES(27,'N28-525-01','N28 - Level 5',1.5644,1.036390000000000101e+02,50);
83
+ INSERT INTO `classrooms` VALUES(28,'N28-527-02','N28 - Level 5',1.5645,1.036390000000000101e+02,50);
84
+ INSERT INTO `classrooms` VALUES(29,'N28A-02-33','N28A - Level 2',1.5646,1.036390000000000101e+02,50);
85
+ INSERT INTO `classrooms` VALUES(30,'N28A-02-34','N28A - Level 2',1.5647,1.036390000000000101e+02,50);
86
+ INSERT INTO `classrooms` VALUES(31,'N28-223-02','N28 - Level 2',1.5638,103.6391,50);
87
+ INSERT INTO `classrooms` VALUES(32,'N28-204-01','N28 - Level 2',1.5639,103.6391,50);
88
+ INSERT INTO `classrooms` VALUES(33,'N28-203-01','N28 - Level 2',1.564,103.6391,50);
89
+ INSERT INTO `classrooms` VALUES(34,'N28-202-01','N28 - Level 2',1.5641,103.6391,50);
90
+ INSERT INTO `classrooms` VALUES(35,'N28-330-01','N28 - Level 3',1.5642,103.6391,50);
91
+ INSERT INTO `classrooms` VALUES(36,'N28-352-01','N28 - Level 3',1.5643,103.6391,50);
92
+ INSERT INTO `classrooms` VALUES(37,'N28-321-02','N28 - Level 3',1.5644,103.6391,50);
93
+ INSERT INTO `classrooms` VALUES(38,'N28-329-01','N28 - Level 3',1.5645,103.6391,50);
94
+ INSERT INTO `classrooms` VALUES(39,'N28-350-03','N28 - Level 3',1.5646,103.6391,50);
95
+ INSERT INTO `classrooms` VALUES(40,'N28-422-01','N28 - Level 4',1.5647,103.6391,50);
96
+ INSERT INTO `classrooms` VALUES(41,'N28-423-01','N28 - Level 4',1.5638,103.6392,50);
97
+ INSERT INTO `classrooms` VALUES(42,'L50-DK3','Block L50 - Centre Point',1.5639,103.6392,50);
98
+ INSERT INTO `classrooms` VALUES(43,'P19-DK4','Block P19 - FKE',1.564,103.6392,50);
99
+ INSERT INTO `classrooms` VALUES(44,'P19-DK5','Block P19 - FKE',1.5641,103.6392,50);
100
+ INSERT INTO `classrooms` VALUES(45,'P19-DK6','Block P19 - FKE',1.5642,103.6392,50);
101
+ INSERT INTO `classrooms` VALUES(46,'PUTERI-COURT','Puteri Court Condominium (Home Demo)',3.128024,101.765479,1000);
102
+ CREATE TABLE students (
103
+ id INTEGER NOT NULL,
104
+ user_id INTEGER,
105
+ student_id VARCHAR(50) NOT NULL,
106
+ name VARCHAR(100) NOT NULL,
107
+ email VARCHAR(100),
108
+ face_encoding TEXT,
109
+ registered_at DATETIME,
110
+ is_active BOOLEAN,
111
+ PRIMARY KEY (id),
112
+ UNIQUE (user_id),
113
+ FOREIGN KEY(user_id) REFERENCES users (id),
114
+ UNIQUE (student_id),
115
+ UNIQUE (email)
116
+ );
117
+ INSERT INTO `students` VALUES(1,1,'B24CS0011','CHUA LIN WEI','chuawei@graduate.utm.my','[15.0, 180.0, 15.0, 242.0, 74.0, 75.0, 180.0, 242.0, 127.0, 136.0, 138.0, 179.0, 93.0, 68.0, 122.0, 148.0, 34.0, 224.0, 223.0, 39.0, 58.0, 228.0, 180.0, 183.0, 91.0, 67.0, 219.0, 4.0, 131.0, 147.0, 136.0, 58.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]','2025-11-22 16:33:50.673821',1);
118
+ CREATE TABLE users (
119
+ id INTEGER NOT NULL,
120
+ matric_no VARCHAR(50) NOT NULL,
121
+ name VARCHAR(100) NOT NULL,
122
+ email VARCHAR(100),
123
+ password_hash VARCHAR(255) NOT NULL,
124
+ role VARCHAR(20) NOT NULL,
125
+ is_active BOOLEAN,
126
+ created_at DATETIME,
127
+ PRIMARY KEY (id),
128
+ UNIQUE (matric_no),
129
+ UNIQUE (email)
130
+ );
131
+ INSERT INTO `users` VALUES(1,'B24CS0011','CHUA LIN WEI','chuawei@graduate.utm.my','scrypt:32768:8:1$hE1y6RkAGBX68GXd$0f613cc449e9b0f9bf2be44cc9914af63d03bc6af78c96baf25081d085e8e64fdd001d01b286afe3cde65fba0ce980d5666c4a233057c7937f6924302e38cd84','student',1,'2025-11-22 16:33:49.914535');
132
+ INSERT INTO `users` VALUES(2,'L001','ZHANG LING HE','miaofifi146@gmail.com','scrypt:32768:8:1$0Lde9MkTKZHggMvp$fd98cbd8955f87ffc01b1e383429b350d50ff4c3e5b05ac7f39bf25dac99aa1f0858f8c5c562decc81c297fd5b1f98bbce7ec69027875bd9882eb052e07d777a','lecturer',1,'2025-11-22 16:34:27.215798');
133
+ INSERT INTO `users` VALUES(3,'L9999','Test Lecturer',NULL,'scrypt:32768:8:1$mVC87j8AGr8uHDsu$4b3a45675a2731d350460403556460e2ebbdd85fcdb204cc9ad81695a0f573f20c36405be345ead28baedc30f21f4cf3fc8e32c14bfc3b83f89d2d4a0edf74bf','lecturer',1,'2025-11-22 17:20:16.122417');
134
+ COMMIT;
auth.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Authentication helper functions and decorators
3
+ """
4
+
5
+ from functools import wraps
6
+ from flask import session, redirect, url_for, flash
7
+ from models import User
8
+
9
+ def login_required(f):
10
+ """Decorator to require login for a route"""
11
+ @wraps(f)
12
+ def decorated_function(*args, **kwargs):
13
+ if 'user_id' not in session:
14
+ flash('Please login to access this page', 'error')
15
+ return redirect(url_for('login_page'))
16
+ return f(*args, **kwargs)
17
+ return decorated_function
18
+
19
+ def role_required(role):
20
+ """Decorator to require specific role for a route"""
21
+ def decorator(f):
22
+ @wraps(f)
23
+ def decorated_function(*args, **kwargs):
24
+ if 'user_id' not in session:
25
+ flash('Please login to access this page', 'error')
26
+ return redirect(url_for('login_page'))
27
+
28
+ user = User.query.get(session['user_id'])
29
+ if not user or user.role != role:
30
+ flash(f'Access denied. This page is for {role}s only.', 'error')
31
+ return redirect(url_for('index'))
32
+
33
+ return f(*args, **kwargs)
34
+ return decorated_function
35
+ return decorator
36
+
37
+ def get_current_user():
38
+ """Get currently logged in user"""
39
+ if 'user_id' in session:
40
+ return User.query.get(session['user_id'])
41
+ return None
clear_database.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Clear all database data and reinitialize with fresh tables
3
+ """
4
+
5
+ from app import app, db
6
+ from models import User, Student, Classroom, AttendanceSession, AttendanceRecord
7
+
8
+ def clear_database():
9
+ """Drop all tables and recreate them"""
10
+
11
+ with app.app_context():
12
+ print("Clearing database...")
13
+ print("="*70)
14
+
15
+ # Drop all tables
16
+ db.drop_all()
17
+ print("[OK] Dropped all tables")
18
+
19
+ # Recreate all tables
20
+ db.create_all()
21
+ print("[OK] Created fresh tables")
22
+
23
+ print("="*70)
24
+ print("Database cleared successfully!")
25
+ print("\nAll data has been removed:")
26
+ print(" - Users: 0")
27
+ print(" - Students: 0")
28
+ print(" - Classrooms: 0")
29
+ print(" - Attendance Sessions: 0")
30
+ print(" - Attendance Records: 0")
31
+ print("="*70)
32
+ print("\nNext steps:")
33
+ print("1. Run: python add_utm_venues.py (to add classrooms)")
34
+ print("2. Run: python add_home_venue.py (to add Puteri Court)")
35
+ print("3. Register new accounts at: http://localhost:5000/register")
36
+ print("="*70)
37
+
38
+ if __name__ == '__main__':
39
+ clear_database()
config.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ class Config:
4
+ """Configuration settings for Attendr Smart Attendance System"""
5
+
6
+ # Flask settings
7
+ SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'
8
+
9
+ # Database settings
10
+ BASE_DIR = os.path.abspath(os.path.dirname(__file__))
11
+ SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
12
+ 'sqlite:///' + os.path.join(BASE_DIR, 'attendr.db')
13
+ SQLALCHEMY_TRACK_MODIFICATIONS = False
14
+
15
+ # Upload folder settings
16
+ UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')
17
+ FACE_ENCODINGS_FOLDER = os.path.join(UPLOAD_FOLDER, 'face_encodings')
18
+ MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB max file size
19
+
20
+ # Face recognition settings
21
+ FACE_RECOGNITION_TOLERANCE = 0.6 # Lower = more strict (0.6 is default)
22
+ FACE_DETECTION_MODEL = 'hog' # 'hog' is faster, 'cnn' is more accurate
23
+
24
+ # Geolocation settings
25
+ GEOLOCATION_RADIUS_METERS = 50 # Classroom detection radius
26
+
27
+ # Attendance Code Settings
28
+ CODE_LENGTH = 6 # Length of attendance code
29
+ CODE_REFRESH_INTERVAL = 10 # Code refresh interval in seconds (10 seconds for demo)
30
+
31
+ # Session Settings
32
+ SESSION_DURATION_HOURS = 3 # Default session duration
33
+
34
+ @staticmethod
35
+ def init_app(app):
36
+ """Initialize application with config"""
37
+ # Create upload folders if they don't exist
38
+ os.makedirs(Config.UPLOAD_FOLDER, exist_ok=True)
39
+ os.makedirs(Config.FACE_ENCODINGS_FOLDER, exist_ok=True)
dump_db_to_sql.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import sqlite3
3
+ import os
4
+
5
+ def dump_sqlite_to_sql(db_path, output_path):
6
+ # Check if DB exists
7
+ if not os.path.exists(db_path):
8
+ print(f"Error: Database file '{db_path}' not found.")
9
+ return
10
+
11
+ try:
12
+ # Connect to the database
13
+ conn = sqlite3.connect(db_path)
14
+
15
+ # Open the output file
16
+ with open(output_path, 'w', encoding='utf-8') as f:
17
+ # Iterate through the dump and write to file
18
+ for line in conn.iterdump():
19
+ f.write('%s\n' % line)
20
+
21
+ print(f"Successfully dumped '{db_path}' to '{output_path}'.")
22
+
23
+ conn.close()
24
+ except Exception as e:
25
+ print(f"An error occurred: {e}")
26
+
27
+ if __name__ == "__main__":
28
+ db_file = "attendr.db"
29
+ sql_file = "attendr.sql"
30
+
31
+ # Get absolute paths
32
+ current_dir = os.getcwd()
33
+ db_path = os.path.join(current_dir, db_file)
34
+ output_path = os.path.join(current_dir, sql_file)
35
+
36
+ print(f"Dumping {db_path}...")
37
+ dump_sqlite_to_sql(db_path, output_path)
face_recognition_module.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Mock Face Recognition Module for Testing (Windows-friendly)
3
+ This is a simplified version that doesn't require dlib/face_recognition
4
+ For production, install the full face_recognition library
5
+ """
6
+
7
+ import numpy as np
8
+ import cv2
9
+ from config import Config
10
+ import logging
11
+ import hashlib
12
+
13
+ # Set up logging
14
+ logging.basicConfig(level=logging.INFO)
15
+ logger = logging.getLogger(__name__)
16
+
17
+ logger.warning("Using MOCK face recognition module - for testing only!")
18
+ logger.warning("Install face_recognition library for production use")
19
+
20
+
21
+ class FaceRecognitionModule:
22
+ """Mock face recognition for testing without dlib dependencies"""
23
+
24
+ def __init__(self, tolerance=None, model='hog'):
25
+ self.tolerance = tolerance or Config.FACE_RECOGNITION_TOLERANCE
26
+ self.model = model
27
+ logger.info(f"Mock Face Recognition Module initialized")
28
+
29
+ def detect_faces(self, image_array):
30
+ """Mock face detection using OpenCV Haar Cascades"""
31
+ try:
32
+ # Convert to grayscale
33
+ gray = cv2.cvtColor(image_array, cv2.COLOR_RGB2GRAY)
34
+
35
+ # Load Haar cascade for face detection
36
+ face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
37
+
38
+ # Detect faces
39
+ faces = face_cascade.detectMultiScale(gray, 1.3, 5)
40
+
41
+ # Convert to format similar to face_recognition library
42
+ face_locations = [(y, x+w, y+h, x) for (x, y, w, h) in faces]
43
+
44
+ logger.info(f"Detected {len(face_locations)} face(s)")
45
+ return face_locations
46
+ except Exception as e:
47
+ logger.error(f"Face detection error: {str(e)}")
48
+ return []
49
+
50
+ def generate_face_encoding(self, image_array):
51
+ """
52
+ Mock encoding generation using image hash
53
+ In production, this would use deep learning-based face encodings
54
+ """
55
+ try:
56
+ face_locations = self.detect_faces(image_array)
57
+
58
+ if len(face_locations) == 0:
59
+ return False, "No face detected in the image. Please ensure your face is clearly visible."
60
+
61
+ if len(face_locations) > 1:
62
+ return False, "Multiple faces detected. Please ensure only one person is in the frame."
63
+
64
+ # Create a simple "encoding" using image hash (for testing only)
65
+ # In production, this would be a 128-dimensional face encoding
66
+ top, right, bottom, left = face_locations[0]
67
+ face_region = image_array[top:bottom, left:right]
68
+
69
+ # Resize to standard size
70
+ face_resized = cv2.resize(face_region, (100, 100))
71
+
72
+ # Create hash-based encoding (mock)
73
+ face_bytes = face_resized.tobytes()
74
+ hash_obj = hashlib.sha256(face_bytes)
75
+ hash_digest = hash_obj.digest() # Get bytes directly
76
+
77
+ # Convert to numpy array (128 dimensions to match real encodings)
78
+ # Use hash bytes to create 128-dimensional vector
79
+ encoding = np.array([float(b) for b in hash_digest[:128]] + [0.0] * (128 - len(hash_digest[:128])))
80
+
81
+ logger.info("Mock face encoding generated")
82
+ return True, encoding
83
+
84
+ except Exception as e:
85
+ logger.error(f"Face encoding error: {str(e)}")
86
+ return False, f"Face encoding failed: {str(e)}"
87
+
88
+ def verify_face(self, captured_encoding, stored_encoding):
89
+ """
90
+ Mock face verification using encoding similarity
91
+ In production, this would use Euclidean distance between face encodings
92
+ """
93
+ try:
94
+ if captured_encoding is None or stored_encoding is None:
95
+ return False, 0.0
96
+
97
+ # Convert to numpy arrays
98
+ if not isinstance(captured_encoding, np.ndarray):
99
+ captured_encoding = np.array(captured_encoding)
100
+ if not isinstance(stored_encoding, np.ndarray):
101
+ stored_encoding = np.array(stored_encoding)
102
+
103
+ # Calculate similarity (mock - using correlation)
104
+ # In production, this would be face_recognition.face_distance()
105
+ correlation = np.corrcoef(captured_encoding, stored_encoding)[0, 1]
106
+
107
+ # Convert to distance (0 = identical, 1 = completely different)
108
+ distance = 1 - abs(correlation)
109
+
110
+ # Calculate confidence
111
+ confidence = (1 - distance) * 100
112
+
113
+ # Check if match
114
+ is_match = distance <= self.tolerance
115
+
116
+ logger.info(f"Mock verification: match={is_match}, confidence={confidence:.2f}%")
117
+
118
+ return is_match, confidence
119
+
120
+ except Exception as e:
121
+ logger.error(f"Face verification error: {str(e)}")
122
+ return False, 0.0
123
+
124
+ def register_face(self, image_array):
125
+ """Complete face registration"""
126
+ try:
127
+ face_locations = self.detect_faces(image_array)
128
+
129
+ if len(face_locations) == 0:
130
+ return False, "No face detected. Please ensure your face is clearly visible and well-lit.", None
131
+
132
+ if len(face_locations) > 1:
133
+ return False, "Multiple faces detected. Please ensure only one person is in the frame.", None
134
+
135
+ success, result = self.generate_face_encoding(image_array)
136
+
137
+ if success:
138
+ return True, result, face_locations[0]
139
+ else:
140
+ return False, result, None
141
+
142
+ except Exception as e:
143
+ logger.error(f"Face registration error: {str(e)}")
144
+ return False, f"Registration failed: {str(e)}", None
145
+
146
+ def compare_faces_batch(self, known_encodings, face_encoding_to_check):
147
+ """Compare against multiple encodings"""
148
+ try:
149
+ matches = []
150
+ for known_encoding in known_encodings:
151
+ is_match, _ = self.verify_face(face_encoding_to_check, known_encoding)
152
+ matches.append(is_match)
153
+ return matches
154
+ except Exception as e:
155
+ logger.error(f"Batch comparison error: {str(e)}")
156
+ return []
157
+
158
+
159
+ # Singleton instance
160
+ face_recognition_module = FaceRecognitionModule()
face_recognition_module_PRODUCTION.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Production Face Recognition Module for Attendr
3
+ This is the REAL implementation using the face_recognition library
4
+ Use this file after installing dlib and face_recognition
5
+
6
+ To activate:
7
+ 1. Install dlib and face_recognition
8
+ 2. Rename face_recognition_module.py to face_recognition_module_MOCK.py
9
+ 3. Rename this file to face_recognition_module.py
10
+ 4. Restart the Flask application
11
+ """
12
+
13
+ import face_recognition
14
+ import numpy as np
15
+ from config import Config
16
+ import logging
17
+
18
+ # Set up logging
19
+ logging.basicConfig(level=logging.INFO)
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class FaceRecognitionModule:
24
+ """Handles all face recognition operations using production library"""
25
+
26
+ def __init__(self, tolerance=None, model='hog'):
27
+ """
28
+ Initialize face recognition module
29
+
30
+ Args:
31
+ tolerance: Face matching tolerance (lower = more strict)
32
+ model: Detection model ('hog' or 'cnn')
33
+ """
34
+ self.tolerance = tolerance or Config.FACE_RECOGNITION_TOLERANCE
35
+ self.model = model or Config.FACE_DETECTION_MODEL
36
+ logger.info(f"PRODUCTION Face Recognition Module initialized with tolerance={self.tolerance}, model={self.model}")
37
+
38
+ def detect_faces(self, image_array):
39
+ """
40
+ Detect faces in an image
41
+
42
+ Args:
43
+ image_array: numpy array of the image
44
+
45
+ Returns:
46
+ List of face locations [(top, right, bottom, left), ...]
47
+ """
48
+ try:
49
+ face_locations = face_recognition.face_locations(image_array, model=self.model)
50
+ logger.info(f"Detected {len(face_locations)} face(s) in image")
51
+ return face_locations
52
+ except Exception as e:
53
+ logger.error(f"Face detection error: {str(e)}")
54
+ return []
55
+
56
+ def generate_face_encoding(self, image_array):
57
+ """
58
+ Generate face encoding from an image
59
+ Implements: CapturedFace(x) - captures and encodes the face
60
+
61
+ Args:
62
+ image_array: numpy array of the image
63
+
64
+ Returns:
65
+ tuple: (success, encoding or error_message)
66
+ """
67
+ try:
68
+ # Detect faces first
69
+ face_locations = self.detect_faces(image_array)
70
+
71
+ if len(face_locations) == 0:
72
+ return False, "No face detected in the image. Please ensure your face is clearly visible."
73
+
74
+ if len(face_locations) > 1:
75
+ return False, "Multiple faces detected. Please ensure only one person is in the frame."
76
+
77
+ # Generate encoding
78
+ encodings = face_recognition.face_encodings(image_array, face_locations)
79
+
80
+ if len(encodings) == 0:
81
+ return False, "Failed to generate face encoding. Please try again with better lighting."
82
+
83
+ encoding = encodings[0]
84
+ logger.info("Face encoding generated successfully")
85
+ return True, encoding
86
+
87
+ except Exception as e:
88
+ logger.error(f"Face encoding error: {str(e)}")
89
+ return False, f"Face encoding failed: {str(e)}"
90
+
91
+ def verify_face(self, captured_encoding, stored_encoding):
92
+ """
93
+ Verify if captured face matches stored encoding
94
+ Implements: MatchStored(x) → FaceMatch(x)
95
+
96
+ Args:
97
+ captured_encoding: Face encoding from live capture
98
+ stored_encoding: Stored face encoding from database
99
+
100
+ Returns:
101
+ tuple: (is_match: bool, confidence: float)
102
+ """
103
+ try:
104
+ if captured_encoding is None or stored_encoding is None:
105
+ logger.warning("One or both encodings are None")
106
+ return False, 0.0
107
+
108
+ # Convert to numpy arrays if needed
109
+ if not isinstance(captured_encoding, np.ndarray):
110
+ captured_encoding = np.array(captured_encoding)
111
+ if not isinstance(stored_encoding, np.ndarray):
112
+ stored_encoding = np.array(stored_encoding)
113
+
114
+ # Calculate face distance (lower = more similar)
115
+ face_distance = face_recognition.face_distance([stored_encoding], captured_encoding)[0]
116
+
117
+ # Convert distance to confidence percentage
118
+ confidence = (1 - face_distance) * 100
119
+
120
+ # Check if match is within tolerance
121
+ is_match = face_distance <= self.tolerance
122
+
123
+ logger.info(f"Face verification: match={is_match}, confidence={confidence:.2f}%, distance={face_distance:.4f}")
124
+
125
+ return is_match, confidence
126
+
127
+ except Exception as e:
128
+ logger.error(f"Face verification error: {str(e)}")
129
+ return False, 0.0
130
+
131
+ def register_face(self, image_array):
132
+ """
133
+ Complete face registration process
134
+
135
+ Args:
136
+ image_array: numpy array of the image
137
+
138
+ Returns:
139
+ tuple: (success, encoding or error_message, face_location)
140
+ """
141
+ try:
142
+ # Detect faces
143
+ face_locations = self.detect_faces(image_array)
144
+
145
+ if len(face_locations) == 0:
146
+ return False, "No face detected. Please ensure your face is clearly visible and well-lit.", None
147
+
148
+ if len(face_locations) > 1:
149
+ return False, "Multiple faces detected. Please ensure only one person is in the frame.", None
150
+
151
+ # Generate encoding
152
+ success, result = self.generate_face_encoding(image_array)
153
+
154
+ if success:
155
+ return True, result, face_locations[0]
156
+ else:
157
+ return False, result, None
158
+
159
+ except Exception as e:
160
+ logger.error(f"Face registration error: {str(e)}")
161
+ return False, f"Registration failed: {str(e)}", None
162
+
163
+ def compare_faces_batch(self, known_encodings, face_encoding_to_check):
164
+ """
165
+ Compare a face encoding against multiple known encodings
166
+ Useful for identifying which student from a list
167
+
168
+ Args:
169
+ known_encodings: List of known face encodings
170
+ face_encoding_to_check: Face encoding to compare
171
+
172
+ Returns:
173
+ List of boolean matches
174
+ """
175
+ try:
176
+ if not isinstance(face_encoding_to_check, np.ndarray):
177
+ face_encoding_to_check = np.array(face_encoding_to_check)
178
+
179
+ matches = face_recognition.compare_faces(
180
+ known_encodings,
181
+ face_encoding_to_check,
182
+ tolerance=self.tolerance
183
+ )
184
+
185
+ return matches
186
+
187
+ except Exception as e:
188
+ logger.error(f"Batch face comparison error: {str(e)}")
189
+ return []
190
+
191
+
192
+ # Singleton instance
193
+ face_recognition_module = FaceRecognitionModule()
fix_sql_quotes.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import re
3
+
4
+ file_path = 'attendr.sql'
5
+
6
+ with open(file_path, 'r', encoding='utf-8') as f:
7
+ content = f.read()
8
+
9
+ # Replace "table_name" with `table_name` in INSERT INTO statements
10
+ # Regex looks for: INSERT INTO "word"
11
+ # Replaces with: INSERT INTO `word`
12
+ # Or I can just remove the quotes since table names are safe. Let's remove them to be safe across different modes if possible, but backticks are safer for MySQL.
13
+ # Let's use backticks.
14
+
15
+ def replace_quotes(match):
16
+ return f'INSERT INTO `{match.group(1)}`'
17
+
18
+ new_content = re.sub(r'INSERT INTO "([^"]+)"', replace_quotes, content)
19
+
20
+ with open(file_path, 'w', encoding='utf-8') as f:
21
+ f.write(new_content)
22
+
23
+ print("Fixed quotes in attendr.sql")
geolocation_module.py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Geolocation Verification Module for Attendr
3
+ Implements KR Rule #2: Geolocation Verification
4
+ ∀x[(Student(x) ∧ IsWithinAllowedArea(x)) → VerifiedLocation(x)]
5
+ """
6
+
7
+ from utils import haversine_distance, validate_coordinates
8
+ from models import Classroom, db
9
+ from config import Config
10
+ import logging
11
+
12
+ # Set up logging
13
+ logging.basicConfig(level=logging.INFO)
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class GeolocationModule:
18
+ """Handles all geolocation verification operations"""
19
+
20
+ def __init__(self, default_radius=None):
21
+ """
22
+ Initialize geolocation module
23
+
24
+ Args:
25
+ default_radius: Default radius in meters for geofencing
26
+ """
27
+ self.default_radius = default_radius or Config.GEOLOCATION_RADIUS_METERS
28
+ logger.info(f"Geolocation Module initialized with default radius={self.default_radius}m")
29
+
30
+ def get_classroom_location(self, classroom_id):
31
+ """
32
+ Get classroom GPS coordinates and radius
33
+
34
+ Args:
35
+ classroom_id: ID of the classroom
36
+
37
+ Returns:
38
+ tuple: (latitude, longitude, radius) or (None, None, None) if not found
39
+ """
40
+ try:
41
+ classroom = Classroom.query.get(classroom_id)
42
+
43
+ if not classroom:
44
+ logger.warning(f"Classroom {classroom_id} not found")
45
+ return None, None, None
46
+
47
+ radius = classroom.radius_meters or self.default_radius
48
+ logger.info(f"Classroom {classroom.name}: lat={classroom.latitude}, lon={classroom.longitude}, radius={radius}m")
49
+
50
+ return classroom.latitude, classroom.longitude, radius
51
+
52
+ except Exception as e:
53
+ logger.error(f"Error fetching classroom location: {str(e)}")
54
+ return None, None, None
55
+
56
+ def calculate_distance(self, lat1, lon1, lat2, lon2):
57
+ """
58
+ Calculate distance between two GPS coordinates
59
+
60
+ Args:
61
+ lat1, lon1: First coordinate
62
+ lat2, lon2: Second coordinate
63
+
64
+ Returns:
65
+ Distance in meters
66
+ """
67
+ try:
68
+ distance = haversine_distance(lat1, lon1, lat2, lon2)
69
+ logger.info(f"Distance calculated: {distance:.2f} meters")
70
+ return distance
71
+ except Exception as e:
72
+ logger.error(f"Distance calculation error: {str(e)}")
73
+ return None
74
+
75
+ def is_within_allowed_area(self, student_lat, student_lon, classroom_id):
76
+ """
77
+ Check if student is within allowed classroom area
78
+ Implements: IsWithinAllowedArea(x)
79
+
80
+ Args:
81
+ student_lat: Student's latitude
82
+ student_lon: Student's longitude
83
+ classroom_id: ID of the classroom
84
+
85
+ Returns:
86
+ tuple: (is_within: bool, distance: float, error_message: str or None)
87
+ """
88
+ try:
89
+ # Validate student coordinates
90
+ valid, error = validate_coordinates(student_lat, student_lon)
91
+ if not valid:
92
+ logger.warning(f"Invalid student coordinates: {error}")
93
+ return False, None, error
94
+
95
+ # Get classroom location
96
+ class_lat, class_lon, radius = self.get_classroom_location(classroom_id)
97
+
98
+ if class_lat is None:
99
+ return False, None, "Classroom location not found"
100
+
101
+ # Calculate distance
102
+ distance = self.calculate_distance(
103
+ float(student_lat),
104
+ float(student_lon),
105
+ class_lat,
106
+ class_lon
107
+ )
108
+
109
+ if distance is None:
110
+ return False, None, "Failed to calculate distance"
111
+
112
+ # Check if within radius
113
+ is_within = distance <= radius
114
+
115
+ logger.info(f"Location verification: within_area={is_within}, distance={distance:.2f}m, allowed_radius={radius}m")
116
+
117
+ if is_within:
118
+ return True, distance, None
119
+ else:
120
+ return False, distance, f"You are {distance:.0f}m away from the classroom (max allowed: {radius}m)"
121
+
122
+ except Exception as e:
123
+ logger.error(f"Location verification error: {str(e)}")
124
+ return False, None, f"Location verification failed: {str(e)}"
125
+
126
+ def verify_location(self, student_lat, student_lon, classroom_id):
127
+ """
128
+ Complete location verification process
129
+ Implements: Student(x) ∧ IsWithinAllowedArea(x) → VerifiedLocation(x)
130
+
131
+ Args:
132
+ student_lat: Student's latitude
133
+ student_lon: Student's longitude
134
+ classroom_id: ID of the classroom
135
+
136
+ Returns:
137
+ dict: Verification result with status, distance, and message
138
+ """
139
+ is_within, distance, error = self.is_within_allowed_area(
140
+ student_lat,
141
+ student_lon,
142
+ classroom_id
143
+ )
144
+
145
+ result = {
146
+ 'verified': is_within,
147
+ 'distance': distance,
148
+ 'message': None,
149
+ 'error': error
150
+ }
151
+
152
+ if is_within:
153
+ result['message'] = f"Location verified! You are {distance:.0f}m from the classroom."
154
+ elif error:
155
+ result['message'] = error
156
+
157
+ return result
158
+
159
+ def get_all_classrooms(self):
160
+ """
161
+ Get all available classrooms
162
+
163
+ Returns:
164
+ List of classroom dictionaries
165
+ """
166
+ try:
167
+ classrooms = Classroom.query.all()
168
+ return [classroom.to_dict() for classroom in classrooms]
169
+ except Exception as e:
170
+ logger.error(f"Error fetching classrooms: {str(e)}")
171
+ return []
172
+
173
+ def add_classroom(self, name, building, latitude, longitude, radius_meters=None):
174
+ """
175
+ Add a new classroom to the system
176
+
177
+ Args:
178
+ name: Classroom name
179
+ building: Building name
180
+ latitude: GPS latitude
181
+ longitude: GPS longitude
182
+ radius_meters: Geofencing radius (optional)
183
+
184
+ Returns:
185
+ tuple: (success, classroom_dict or error_message)
186
+ """
187
+ try:
188
+ # Validate coordinates
189
+ valid, error = validate_coordinates(latitude, longitude)
190
+ if not valid:
191
+ return False, error
192
+
193
+ # Create classroom
194
+ classroom = Classroom(
195
+ name=name,
196
+ building=building,
197
+ latitude=float(latitude),
198
+ longitude=float(longitude),
199
+ radius_meters=radius_meters or self.default_radius
200
+ )
201
+
202
+ db.session.add(classroom)
203
+ db.session.commit()
204
+
205
+ logger.info(f"Classroom added: {name} in {building}")
206
+ return True, classroom.to_dict()
207
+
208
+ except Exception as e:
209
+ db.session.rollback()
210
+ logger.error(f"Error adding classroom: {str(e)}")
211
+ return False, f"Failed to add classroom: {str(e)}"
212
+
213
+
214
+ # Singleton instance
215
+ geolocation_module = GeolocationModule()
index.html ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Attendr - Smart Attendance System</title>
8
+ <link rel="stylesheet" href="style.css">
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link
12
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@700;800&display=swap"
13
+ rel="stylesheet">
14
+ </head>
15
+
16
+ <body>
17
+ <!-- Navigation -->
18
+ <!-- Navigation -->
19
+ {% include '_navbar.html' %}
20
+
21
+ <!-- Hero Section -->
22
+ <section class="section">
23
+ <div class="container">
24
+ <div class="text-center" style="max-width: 900px; margin: 0 auto;">
25
+ <h1 class="fade-in">Smart Attendance System</h1>
26
+ <p class="fade-in" style="font-size: 1.25rem; margin-bottom: 2rem; color: var(--color-text-secondary);">
27
+ AI-powered attendance verification using facial recognition, geolocation, and auto-refresh codes
28
+ </p>
29
+
30
+ <!-- Feature Highlights -->
31
+ <div class="grid grid-3 mt-3 mb-3">
32
+ <div class="glass-card fade-in" style="animation-delay: 0.1s;">
33
+ <div style="font-size: 3rem; margin-bottom: 1rem;">🎭</div>
34
+ <h3>Face Recognition</h3>
35
+ <p>Biometric verification ensures only registered students can mark attendance</p>
36
+ </div>
37
+
38
+ <div class="glass-card fade-in" style="animation-delay: 0.2s;">
39
+ <div style="font-size: 3rem; margin-bottom: 1rem;">📍</div>
40
+ <h3>GPS Verification</h3>
41
+ <p>Location-based validation confirms physical presence in the classroom</p>
42
+ </div>
43
+
44
+ <div class="glass-card fade-in" style="animation-delay: 0.3s;">
45
+ <div style="font-size: 3rem; margin-bottom: 1rem;">🔄</div>
46
+ <h3>Auto-Refresh Codes</h3>
47
+ <p>Time-limited codes prevent sharing and proxy attendance</p>
48
+ </div>
49
+ </div>
50
+
51
+ <!-- CTA Buttons -->
52
+ <div class="flex-center gap-2 mt-3">
53
+ <a href="/student" class="btn btn-primary">
54
+ <span>👤</span>
55
+ <span>Student Portal</span>
56
+ </a>
57
+ <a href="/lecturer" class="btn btn-secondary">
58
+ <span>👨‍🏫</span>
59
+ <span>Lecturer Dashboard</span>
60
+ </a>
61
+ <a href="/register" class="btn btn-outline">
62
+ <span>📝</span>
63
+ <span>Register</span>
64
+ </a>
65
+ </div>
66
+
67
+ <!-- System Overview -->
68
+ <div class="glass-card mt-3 text-left">
69
+ <h2 class="mb-2">How It Works</h2>
70
+ <div class="grid grid-2 gap-2">
71
+ <div>
72
+ <h3 style="color: var(--color-primary-light);">For Students</h3>
73
+ <ol style="padding-left: 1.5rem; color: var(--color-text-secondary);">
74
+ <li style="margin-bottom: 0.5rem;">Register your account and face</li>
75
+ <li style="margin-bottom: 0.5rem;">Login and enable camera/GPS permissions</li>
76
+ <li style="margin-bottom: 0.5rem;">System verifies your face and location</li>
77
+ <li style="margin-bottom: 0.5rem;">Enter the attendance code from lecturer</li>
78
+ <li style="margin-bottom: 0.5rem;">Attendance marked instantly!</li>
79
+ </ol>
80
+ </div>
81
+ <div>
82
+ <h3 style="color: var(--color-secondary);">For Lecturers</h3>
83
+ <ol style="padding-left: 1.5rem; color: var(--color-text-secondary);">
84
+ <li style="margin-bottom: 0.5rem;">Register lecturer account</li>
85
+ <li style="margin-bottom: 0.5rem;">Login and create attendance session</li>
86
+ <li style="margin-bottom: 0.5rem;">Display auto-refreshing code to students</li>
87
+ <li style="margin-bottom: 0.5rem;">Monitor real-time attendance updates</li>
88
+ <li style="margin-bottom: 0.5rem;">Export reports for record-keeping</li>
89
+ </ol>
90
+ </div>
91
+ </div>
92
+ </div>
93
+
94
+ <!-- Knowledge Representation -->
95
+ <div class="glass-card mt-3 text-left">
96
+ <h2 class="mb-2">AI Logic & Security</h2>
97
+ <p style="color: var(--color-text-secondary); margin-bottom: 1.5rem;">
98
+ Attendr implements five Knowledge Representation (KR) rules using First-Order Logic to ensure
99
+ secure, fraud-proof attendance:
100
+ </p>
101
+ <div class="grid grid-2 gap-2">
102
+ <div class="alert alert-info">
103
+ <div>
104
+ <strong>KR Rule #1: Face Recognition</strong><br>
105
+ <small>∀x ((CapturedFace(x) ∧ MatchStored(x)) → FaceMatch(x))</small>
106
+ </div>
107
+ </div>
108
+ <div class="alert alert-info">
109
+ <div>
110
+ <strong>KR Rule #2: Location Verification</strong><br>
111
+ <small>∀x[(Student(x) ∧ IsWithinAllowedArea(x)) → VerifiedLocation(x)]</small>
112
+ </div>
113
+ </div>
114
+ <div class="alert alert-info">
115
+ <div>
116
+ <strong>KR Rule #3: Device Readiness</strong><br>
117
+ <small>∀d ((CameraOn(d) ∧ GPSOn(d)) → StartVerification(d))</small>
118
+ </div>
119
+ </div>
120
+ <div class="alert alert-info">
121
+ <div>
122
+ <strong>KR Rule #4: Attendance Validation</strong><br>
123
+ <small>∀x ((FaceMatch(x) ∧ LocationValid(x)) → GrantCodeAccess(x))</small>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ <div class="alert alert-success mt-2">
128
+ <div>
129
+ <strong>KR Rule #5: Auto-Refresh Code Confirmation</strong><br>
130
+ <small>∀x ((ValidCodeEntry(User,x) ∧ WithinCycle(Code,x)) → MarkPresent(System,x))</small>
131
+ </div>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ </div>
136
+ </section>
137
+
138
+ <script>
139
+ // Add stagger animation to cards
140
+ document.addEventListener('DOMContentLoaded', () => {
141
+ const cards = document.querySelectorAll('.fade-in');
142
+ cards.forEach((card, index) => {
143
+ card.style.animationDelay = `${index * 0.1}s`;
144
+ });
145
+ });
146
+
147
+ // Logout function
148
+ async function logout() {
149
+ if (!confirm('Are you sure you want to logout?')) return;
150
+
151
+ try {
152
+ const response = await fetch('/api/auth/logout', {
153
+ method: 'POST',
154
+ headers: { 'Content-Type': 'application/json' }
155
+ });
156
+
157
+ const data = await response.json();
158
+
159
+ if (data.success) {
160
+ alert(data.message);
161
+ window.location.href = '/';
162
+ } else {
163
+ alert('Logout failed');
164
+ }
165
+ } catch (error) {
166
+ alert('Logout failed');
167
+ }
168
+ }
169
+ </script>
170
+ </body>
171
+
172
+ </html>
init_db.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Database initialization script for Attendr
3
+ Creates tables and seeds sample data
4
+ """
5
+
6
+ from app import app, db
7
+ from models import Student, Classroom, AttendanceSession, AttendanceRecord
8
+ from datetime import datetime
9
+
10
+ def init_database():
11
+ """Initialize database with tables and sample data"""
12
+
13
+ with app.app_context():
14
+ # Drop all tables (use with caution in production!)
15
+ print("Dropping existing tables...")
16
+ db.drop_all()
17
+
18
+ # Create all tables
19
+ print("Creating tables...")
20
+ db.create_all()
21
+
22
+ # Seed sample classrooms (UTM locations)
23
+ print("Seeding sample classrooms...")
24
+
25
+ classrooms = [
26
+ Classroom(
27
+ name="N28-01-01",
28
+ building="N28 (Faculty of Computing)",
29
+ latitude=1.5586, # UTM Skudai approximate coordinates
30
+ longitude=103.6376,
31
+ radius_meters=50
32
+ ),
33
+ Classroom(
34
+ name="N28-02-03",
35
+ building="N28 (Faculty of Computing)",
36
+ latitude=1.5588,
37
+ longitude=103.6378,
38
+ radius_meters=50
39
+ ),
40
+ Classroom(
41
+ name="V01-LT1",
42
+ building="V01 (Lecture Hall)",
43
+ latitude=1.5590,
44
+ longitude=103.6380,
45
+ radius_meters=75
46
+ ),
47
+ Classroom(
48
+ name="C22-Lab A",
49
+ building="C22 (Computer Lab)",
50
+ latitude=1.5584,
51
+ longitude=103.6374,
52
+ radius_meters=40
53
+ )
54
+ ]
55
+
56
+ for classroom in classrooms:
57
+ db.session.add(classroom)
58
+
59
+ db.session.commit()
60
+ print(f"[OK] Created {len(classrooms)} classrooms")
61
+
62
+ # Create a sample student for testing (without face encoding)
63
+ print("Creating sample student...")
64
+ sample_student = Student(
65
+ student_id="A20EC0001",
66
+ name="Test Student",
67
+ email="test@graduate.utm.my",
68
+ is_active=True
69
+ )
70
+ db.session.add(sample_student)
71
+ db.session.commit()
72
+ print("[OK] Created sample student (face registration required)")
73
+
74
+ print("\n" + "="*50)
75
+ print("Database initialization complete!")
76
+ print("="*50)
77
+ print("\nSample Classrooms:")
78
+ for classroom in classrooms:
79
+ print(f" - {classroom.name} ({classroom.building})")
80
+ print(f"\nSample Student: {sample_student.student_id} - {sample_student.name}")
81
+ print("\nNext steps:")
82
+ print("1. Run the Flask app: python app.py")
83
+ print("2. Register student faces at: http://localhost:5000/register")
84
+ print("3. Create attendance session at: http://localhost:5000/lecturer")
85
+ print("4. Mark attendance at: http://localhost:5000/student")
86
+ print("="*50)
87
+
88
+ if __name__ == '__main__':
89
+ init_database()
lecturer.html ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Lecturer Dashboard - Attendr</title>
8
+ <link rel="stylesheet" href="style.css">
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@700;800&display=swap"
11
+ rel="stylesheet">
12
+ </head>
13
+
14
+ <body>
15
+ <!-- Navigation -->
16
+ <!-- Navigation -->
17
+ {% include '_navbar.html' %}
18
+
19
+ <!-- Main Content -->
20
+ <section class="section">
21
+ <div class="container">
22
+ <h1 class="text-center mb-3">Lecturer Dashboard</h1>
23
+
24
+ <div class="grid grid-2 gap-2">
25
+ <!-- Left Column: Session Management -->
26
+ <div>
27
+ <!-- Create Session -->
28
+ <div id="createSessionCard" class="glass-card mb-2">
29
+ <h2>Create Attendance Session</h2>
30
+
31
+ <div class="form-group">
32
+ <label class="form-label">Course Name</label>
33
+ <select id="courseName" class="form-select">
34
+ <option value="">Select a course...</option>
35
+
36
+ <optgroup label="CORE COURSES">
37
+ <option value="SECI1013 - Discrete Structure">SECI1013 - Discrete Structure
38
+ </option>
39
+ <option value="SECJ1013 - Programming Technique I">SECJ1013 - Programming
40
+ Technique
41
+ I</option>
42
+ <option value="SECR1013 - Digital Logic">SECR1013 - Digital Logic</option>
43
+ <option value="SECP1513 - Technology & Information System">SECP1513 - Technology
44
+ &
45
+ Information System</option>
46
+ <option value="SECI1113 - Computational Mathematics">SECI1113 - Computational
47
+ Mathematics</option>
48
+ <option value="SECI2143 - Probability & Statistical Data Analysis">SECI2143 -
49
+ Probability & Statistical Data Analysis</option>
50
+ <option value="SECJ1023 - Programming Technique II">SECJ1023 - Programming
51
+ Technique
52
+ II</option>
53
+ <option value="SECR1033 - Computer Organisation and Architecture">SECR1033 -
54
+ Computer Organisation and Architecture</option>
55
+ <option value="SECD2523 - Database">SECD2523 - Database</option>
56
+ <option value="SECD2613 - System Analysis and Design">SECD2613 - System Analysis
57
+ and
58
+ Design</option>
59
+ <option value="SECJ2013 - Data Structure and Algorithm">SECJ2013 - Data
60
+ Structure
61
+ and Algorithm</option>
62
+ <option value="SECR1213 - Network Communications">SECR1213 - Network
63
+ Communications
64
+ </option>
65
+ <option value="SECV2113 - Human Computer Interaction">SECV2113 - Human Computer
66
+ Interaction</option>
67
+ <option value="SECJ2203 - Software Engineering">SECJ2203 - Software Engineering
68
+ </option>
69
+ <option value="SECV1223 - Web Programming">SECV1223 - Web Programming</option>
70
+ <option value="SECR2043 - Operating Systems">SECR2043 - Operating Systems
71
+ </option>
72
+ <option value="SECJ2154 - Object Oriented Programming">SECJ2154 - Object
73
+ Oriented
74
+ Programming</option>
75
+ <option value="SECR3032 - Computer Networks and Security Project I">SECR3032 -
76
+ Computer Networks and Security Project I</option>
77
+ <option value="SECJ3203 - Theory of Computer Science">SECJ3203 - Theory of
78
+ Computer
79
+ Science</option>
80
+ <option value="SECR4118 - Industrial Training">SECR4118 - Industrial Training
81
+ </option>
82
+ <option value="SECR4114 - Industrial Training Report">SECR4114 - Industrial
83
+ Training
84
+ Report</option>
85
+ <option value="SECR4134 - Computer Networks and Security Project II">SECR4134 -
86
+ Computer Networks and Security Project II</option>
87
+ <option value="SCSD3761 - Technopreneurship Seminar">SCSD3761 -
88
+ Technopreneurship
89
+ Seminar</option>
90
+ </optgroup>
91
+
92
+ <optgroup label="ELECTIVES - SECRH">
93
+ <option value="SECR3104 - Applications Development">SECR3104 - Applications
94
+ Development</option>
95
+ <option value="SECJ3553 - Artificial Intelligence">SECJ3553 - Artificial
96
+ Intelligence</option>
97
+ <option value="SECR2242 - Computer Networks">SECR2242 - Computer Networks
98
+ </option>
99
+ <option value="SECR2941 - Computer Networks Lab">SECR2941 - Computer Networks
100
+ Lab
101
+ </option>
102
+ <option value="SECR3241 - Internetworking Technology">SECR3241 - Internetworking
103
+ Technology</option>
104
+ <option value="SECR3941 - Internetworking Technology Lab">SECR3941 -
105
+ Internetworking
106
+ Technology Lab</option>
107
+ <option value="SECR3413 - Computer Security">SECR3413 - Computer Security
108
+ </option>
109
+ <option value="SECR3443 - Cryptography">SECR3443 - Cryptography</option>
110
+ <option value="SECR3223 - High Performance & Parallel Computing">SECR3223 - High
111
+ Performance & Parallel Computing</option>
112
+ <option value="SECR3253 - Network Programming">SECR3253 - Network Programming
113
+ </option>
114
+ <option value="SECR3263 - Wireless Sensor Network">SECR3263 - Wireless Sensor
115
+ Network</option>
116
+ <option value="SECR4453 - Network Security">SECR4453 - Network Security</option>
117
+ <option value="SECR4483 - Secure Programming">SECR4483 - Secure Programming
118
+ </option>
119
+ <option value="SECR4973 - Special Topics on Computer Network & Security">
120
+ SECR4973 -
121
+ Special Topics on Computer Network & Security</option>
122
+ </optgroup>
123
+
124
+ <optgroup label="ELECTIVES - PRISMS">
125
+ <option value="SECR5013 - Cryptographic Engineering">SECR5013 - Cryptographic
126
+ Engineering</option>
127
+ <option value="SECR5023 - Digital Forensics">SECR5023 - Digital Forensics
128
+ </option>
129
+ <option value="SECR5033 - Information Security Governance and Risk Management">
130
+ SECR5033 - Information Security Governance and Risk Management</option>
131
+ <option value="SECR5043 - Cloud Computing Security">SECR5043 - Cloud Computing
132
+ Security</option>
133
+ <option value="SECR5053 - Penetration Testing">SECR5053 - Penetration Testing
134
+ </option>
135
+ <option value="SECJ5013 - Secure Software Engineering">SECJ5013 - Secure
136
+ Software
137
+ Engineering</option>
138
+ <option value="SECJ5023 - Advanced Theory of Computer Science">SECJ5023 -
139
+ Advanced
140
+ Theory of Computer Science</option>
141
+ <option value="SECJ5033 - Advanced Data Structure and Algorithms">SECJ5033 -
142
+ Advanced Data Structure and Algorithms</option>
143
+ <option value="SECJ5043 - Advanced Artificial Intelligence">SECJ5043 - Advanced
144
+ Artificial Intelligence</option>
145
+ <option value="SECP5013 - Advanced Analytics for Data Science">SECP5013 -
146
+ Advanced
147
+ Analytics for Data Science</option>
148
+ <option value="SECP5023 - Big Data Management">SECP5023 - Big Data Management
149
+ </option>
150
+ <option value="SECP5033 - Business Intelligence and Analytics">SECP5033 -
151
+ Business
152
+ Intelligence and Analytics</option>
153
+ <option value="SECP5043 - Data Science Governance">SECP5043 - Data Science
154
+ Governance</option>
155
+ <option value="SECP5053 - Massive Mining and Streaming">SECP5053 - Massive
156
+ Mining
157
+ and Streaming</option>
158
+ <option value="SECP5063 - Statistics for Data Science">SECP5063 - Statistics for
159
+ Data Science</option>
160
+ </optgroup>
161
+
162
+ <optgroup label="UNIVERSITY GENERAL COURSES">
163
+ <option value="UHIS1022 - Philosophy and Current Issues">UHIS1022 - Philosophy
164
+ and
165
+ Current Issues</option>
166
+ <option value="UHMS1182 - Appreciation of Ethics and Civilisation">UHMS1182 -
167
+ Appreciation of Ethics and Civilisation</option>
168
+ <option value="UHLM1012 - Malaysia Language for Communication">UHLM1012 -
169
+ Malaysia
170
+ Language for Communication</option>
171
+ <option value="ULRS3032 - Entrepreneurship and Innovation">ULRS3032 -
172
+ Entrepreneurship and Innovation</option>
173
+ <option value="ULRS1012 - Value and Identity">ULRS1012 - Value and Identity
174
+ </option>
175
+ <option value="UKQF2xx2 - Service Learning Co-curriculum Elective">UKQF2xx2 -
176
+ Service Learning Co-curriculum Elective</option>
177
+ <option value="UHLB2122 - Academic Communication Skills">UHLB2122 - Academic
178
+ Communication Skills</option>
179
+ <option value="UHLB3132 - Professional Communication Skills">UHLB3132 -
180
+ Professional
181
+ Communication Skills</option>
182
+ <option value="UHLx1112 - Foreign Language Elective">UHLx1112 - Foreign Language
183
+ Elective</option>
184
+ </optgroup>
185
+ </select>
186
+ </div>
187
+
188
+ <div class="form-group">
189
+ <label class="form-label">Lecturer Name</label>
190
+ <input type="text" id="lecturerName" class="form-input" placeholder="Your name">
191
+ </div>
192
+
193
+ <div class="form-group">
194
+ <label class="form-label">Classroom</label>
195
+ <select id="classroomSelect" class="form-select">
196
+ <option value="">Loading classrooms...</option>
197
+ </select>
198
+ </div>
199
+
200
+ <button id="createSessionBtn" class="btn btn-primary">Create Session</button>
201
+ </div>
202
+
203
+ <!-- Active Sessions List -->
204
+ <div class="glass-card">
205
+ <h3>Active Sessions</h3>
206
+ <div id="sessionsList" class="mt-2">
207
+ <p class="text-muted">No active sessions</p>
208
+ </div>
209
+ </div>
210
+ </div>
211
+
212
+ <!-- Right Column: Current Session Display -->
213
+ <div>
214
+ <!-- Attendance Code Display -->
215
+ <div id="codeDisplayCard" class="glass-card mb-2 hidden">
216
+ <h2 class="text-center">Attendance Code</h2>
217
+ <div class="code-display" id="attendanceCodeDisplay">------</div>
218
+ <div class="text-center">
219
+ <p class="text-muted" id="codeTimer">Refreshes in: --s</p>
220
+ <p class="text-muted" style="font-size: 0.875rem;">Code refreshes every 10 seconds</p>
221
+ </div>
222
+ <button id="endSessionBtn" class="btn btn-outline mt-2" style="width: 100%;">End
223
+ Session</button>
224
+ </div>
225
+
226
+ <!-- Attendance Statistics -->
227
+ <div id="statsCard" class="glass-card mb-2 hidden">
228
+ <h3>Attendance Statistics</h3>
229
+ <div class="grid grid-2 gap-2 mt-2">
230
+ <div class="text-center">
231
+ <div style="font-size: 2.5rem; font-weight: 700; color: var(--color-success);"
232
+ id="presentCount">0</div>
233
+ <p class="text-muted">Present</p>
234
+ </div>
235
+ <div class="text-center">
236
+ <div style="font-size: 2.5rem; font-weight: 700; color: var(--color-primary);"
237
+ id="sessionDuration">0m</div>
238
+ <p class="text-muted">Duration</p>
239
+ </div>
240
+ </div>
241
+ </div>
242
+
243
+ <!-- Real-time Attendance List -->
244
+ <div id="attendanceListCard" class="glass-card hidden">
245
+ <div
246
+ style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
247
+ <h3 style="margin: 0;">Attendance Records</h3>
248
+ <button id="exportBtn" class="btn btn-secondary"
249
+ style="padding: 0.5rem 1rem; font-size: 0.875rem;">📥 Export</button>
250
+ </div>
251
+ <div id="attendanceList" style="max-height: 400px; overflow-y: auto;">
252
+ <p class="text-muted">No attendance records yet</p>
253
+ </div>
254
+ </div>
255
+ </div>
256
+ </div>
257
+ </div>
258
+ </section>
259
+
260
+ <!-- Loading Overlay -->
261
+ <div id="loadingOverlay" class="loading-overlay hidden">
262
+ <div class="text-center">
263
+ <div class="spinner"></div>
264
+ <p class="mt-2" id="loadingText">Processing...</p>
265
+ </div>
266
+ </div>
267
+
268
+ <!-- Error/Success Messages -->
269
+ <div id="messageDisplay" class="hidden"
270
+ style="position: fixed; top: 100px; right: 20px; z-index: 9999; max-width: 400px;"></div>
271
+
272
+ <script src="{{ url_for('static', filename='js/lecturer.js') }}"></script>
273
+ <script>
274
+ async function logout() {
275
+ if (!confirm('Are you sure you want to logout?')) return;
276
+
277
+ try {
278
+ const response = await fetch('/api/auth/logout', {
279
+ method: 'POST',
280
+ headers: { 'Content-Type': 'application/json' }
281
+ });
282
+
283
+ const data = await response.json();
284
+
285
+ if (data.success) {
286
+ alert(data.message);
287
+ window.location.href = '/';
288
+ } else {
289
+ alert('Logout failed');
290
+ }
291
+ } catch (error) {
292
+ alert('Logout failed');
293
+ }
294
+ }
295
+ </script>
296
+ </body>
297
+
298
+ </html>
lecturer_history.html ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Session History - Attendr</title>
8
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@700;800&display=swap"
11
+ rel="stylesheet">
12
+ </head>
13
+
14
+ <body>
15
+ <!-- Navigation -->
16
+ <!-- Navigation -->
17
+ {% include '_navbar.html' %}
18
+
19
+ <!-- Main Content -->
20
+ <section class="section">
21
+ <div class="container">
22
+ <h1 class="text-center mb-3">📊 Session History</h1>
23
+
24
+ <!-- Lecturer Info Card -->
25
+ <div class="glass-card mb-2">
26
+ <div class="flex-between">
27
+ <div>
28
+ <h3 id="lecturerName">Loading...</h3>
29
+ <p class="text-muted" id="lecturerId"></p>
30
+ </div>
31
+ <div class="text-right">
32
+ <div style="font-size: 3rem; color: var(--color-primary);" id="totalSessions">0</div>
33
+ <p class="text-muted">Total Sessions</p>
34
+ </div>
35
+ </div>
36
+ </div>
37
+
38
+ <!-- Session Records -->
39
+ <div id="historyContainer">
40
+ <div class="text-center mt-3">
41
+ <div class="spinner"></div>
42
+ <p class="text-muted mt-2">Loading session history...</p>
43
+ </div>
44
+ </div>
45
+
46
+ <!-- Empty State -->
47
+ <div id="emptyState" class="glass-card text-center hidden" style="padding: 3rem;">
48
+ <div style="font-size: 4rem; margin-bottom: 1rem;">📭</div>
49
+ <h2>No Sessions Yet</h2>
50
+ <p class="text-muted mt-2">You haven't created any attendance sessions yet. Go to the dashboard to
51
+ create your first session!</p>
52
+ <a href="/lecturer" class="btn btn-primary mt-2">Create Session</a>
53
+ </div>
54
+ </div>
55
+ </section>
56
+
57
+ <script>
58
+ // Load session history on page load
59
+ document.addEventListener('DOMContentLoaded', loadHistory);
60
+
61
+ async function loadHistory() {
62
+ try {
63
+ const response = await fetch('/api/lecturer/history');
64
+ const data = await response.json();
65
+
66
+ if (data.success) {
67
+ displayHistory(data.data);
68
+ } else {
69
+ showError(data.error);
70
+ }
71
+ } catch (error) {
72
+ showError('Failed to load session history');
73
+ }
74
+ }
75
+
76
+ function displayHistory(data) {
77
+ // Update lecturer info
78
+ document.getElementById('lecturerName').textContent = data.lecturer.name;
79
+ document.getElementById('lecturerId').textContent = `Matric: ${data.lecturer.matric_no}`;
80
+ document.getElementById('totalSessions').textContent = data.total_sessions;
81
+
82
+ const container = document.getElementById('historyContainer');
83
+
84
+ if (data.history.length === 0) {
85
+ container.innerHTML = '';
86
+ document.getElementById('emptyState').classList.remove('hidden');
87
+ return;
88
+ }
89
+
90
+ // Store history data globally for export function
91
+ window.sessionHistory = data.history;
92
+
93
+ // Build history cards
94
+ let html = '<div class="grid grid-1 gap-2">';
95
+
96
+ data.history.forEach(session => {
97
+ const startDate = new Date(session.start_time);
98
+ const endDate = session.end_time ? new Date(session.end_time) : null;
99
+ const statusBadge = session.is_active
100
+ ? '<span class="badge badge-success">🟢 Active</span>'
101
+ : '<span class="badge" style="background: rgba(100,100,100,0.2);">⚫ Ended</span>';
102
+
103
+ html += `
104
+ <div class="glass-card">
105
+ <div class="flex-between mb-1">
106
+ <div>
107
+ <h3>${session.course_name}</h3>
108
+ <p class="text-muted">${session.classroom} - ${session.building}</p>
109
+ </div>
110
+ ${statusBadge}
111
+ </div>
112
+
113
+ <div class="grid grid-3 gap-1 mt-2" style="font-size: 0.9rem;">
114
+ <div>
115
+ <strong>📅 Started:</strong><br>
116
+ ${startDate.toLocaleDateString()} ${startDate.toLocaleTimeString()}
117
+ </div>
118
+ <div>
119
+ <strong>⏱️ Duration:</strong><br>
120
+ ${session.duration_minutes} minutes
121
+ </div>
122
+ <div>
123
+ <strong>👥 Attendance:</strong><br>
124
+ ${session.present_count} / ${session.total_attendance} students
125
+ </div>
126
+ </div>
127
+
128
+ ${session.attendance_records.length > 0 ? `
129
+ <div class="mt-2">
130
+ <div class="grid grid-2 gap-1">
131
+ <button class="btn btn-outline" onclick="toggleDetails(${session.id})">
132
+ 📋 View Details (${session.attendance_records.length})
133
+ </button>
134
+ <button class="btn btn-secondary" onclick="exportSession(${session.id})">
135
+ 📥 Export CSV
136
+ </button>
137
+ </div>
138
+ <div id="details-${session.id}" class="hidden mt-2" style="max-height: 300px; overflow-y: auto;">
139
+ <table style="width: 100%; font-size: 0.875rem;">
140
+ <thead>
141
+ <tr style="border-bottom: 1px solid rgba(255,255,255,0.1);">
142
+ <th style="padding: 0.5rem; text-align: left;">Student ID</th>
143
+ <th style="padding: 0.5rem; text-align: left;">Name</th>
144
+ <th style="padding: 0.5rem; text-align: center;">Time</th>
145
+ <th style="padding: 0.5rem; text-align: center;">Verification</th>
146
+ </tr>
147
+ </thead>
148
+ <tbody>
149
+ ${session.attendance_records.map(record => {
150
+ const markedTime = new Date(record.marked_at);
151
+ return `
152
+ <tr style="border-bottom: 1px solid rgba(255,255,255,0.05);">
153
+ <td style="padding: 0.5rem;">${record.student_id}</td>
154
+ <td style="padding: 0.5rem;">${record.student_name}</td>
155
+ <td style="padding: 0.5rem; text-align: center;">${markedTime.toLocaleTimeString()}</td>
156
+ <td style="padding: 0.5rem; text-align: center;">
157
+ <span class="badge ${record.face_verified ? 'badge-success' : 'badge-error'}" style="font-size: 0.7rem;">Face</span>
158
+ <span class="badge ${record.location_verified ? 'badge-success' : 'badge-error'}" style="font-size: 0.7rem;">GPS</span>
159
+ <span class="badge ${record.code_verified ? 'badge-success' : 'badge-error'}" style="font-size: 0.7rem;">Code</span>
160
+ </td>
161
+ </tr>
162
+ `;
163
+ }).join('')}
164
+ </tbody>
165
+ </table>
166
+ </div>
167
+ </div>
168
+ ` : '<p class="text-muted mt-2">No attendance records</p>'}
169
+ </div>
170
+ `;
171
+ });
172
+
173
+ html += '</div>';
174
+ container.innerHTML = html;
175
+ }
176
+
177
+ function toggleDetails(sessionId) {
178
+ const details = document.getElementById(`details-${sessionId}`);
179
+ details.classList.toggle('hidden');
180
+ }
181
+
182
+ function exportSession(sessionId) {
183
+ const session = window.sessionHistory.find(s => s.id === sessionId);
184
+ if (!session) return;
185
+
186
+ const rows = [
187
+ ['Student ID', 'Name', 'Time', 'Face Verified', 'Location Verified', 'Code Verified']
188
+ ];
189
+
190
+ session.attendance_records.forEach(record => {
191
+ const dateObj = new Date(record.marked_at);
192
+ const time = `${dateObj.toLocaleDateString()} ${dateObj.toLocaleTimeString()}`;
193
+ rows.push([
194
+ record.student_id,
195
+ record.student_name,
196
+ time,
197
+ record.face_verified ? 'Yes' : 'No',
198
+ record.location_verified ? 'Yes' : 'No',
199
+ record.code_verified ? 'Yes' : 'No'
200
+ ]);
201
+ });
202
+
203
+ let csvContent = "data:text/csv;charset=utf-8,"
204
+ + rows.map(e => e.map(cell => `"${cell}"`).join(",")).join("\n");
205
+
206
+ const encodedUri = encodeURI(csvContent);
207
+ const link = document.createElement("a");
208
+ link.setAttribute("href", encodedUri);
209
+ link.setAttribute("download", `attendance_${session.course_name}_${new Date().toISOString().slice(0, 10)}.csv`);
210
+ document.body.appendChild(link);
211
+ link.click();
212
+ document.body.removeChild(link);
213
+ }
214
+
215
+ function showError(message) {
216
+ const container = document.getElementById('historyContainer');
217
+ container.innerHTML = `
218
+ <div class="alert alert-error">
219
+ ${message}
220
+ </div>
221
+ `;
222
+ }
223
+
224
+ async function logout() {
225
+ if (!confirm('Are you sure you want to logout?')) return;
226
+
227
+ try {
228
+ const response = await fetch('/api/auth/logout', {
229
+ method: 'POST',
230
+ headers: { 'Content-Type': 'application/json' }
231
+ });
232
+
233
+ const data = await response.json();
234
+
235
+ if (data.success) {
236
+ alert(data.message);
237
+ window.location.href = '/';
238
+ } else {
239
+ alert('Logout failed');
240
+ }
241
+ } catch (error) {
242
+ alert('Logout failed');
243
+ }
244
+ }
245
+ </script>
246
+ </body>
247
+
248
+ </html>
login.html ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Login - Attendr</title>
8
+ <link rel="stylesheet" href="style.css">
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@700;800&display=swap"
11
+ rel="stylesheet">
12
+ </head>
13
+
14
+ <body>
15
+ <!-- Navigation -->
16
+ <!-- Navigation -->
17
+ {% include '_navbar.html' %}
18
+
19
+ <!-- Main Content -->
20
+ <section class="section">
21
+ <div class="container" style="max-width: 500px;">
22
+ <h1 class="text-center mb-3">Login to Attendr</h1>
23
+ <p class="text-center text-muted mb-3">
24
+ Access your student or lecturer account
25
+ </p>
26
+
27
+ <!-- Login Form -->
28
+ <div class="glass-card">
29
+ <div class="form-group">
30
+ <label class="form-label">Matric Number</label>
31
+ <input type="text" id="matricNo" class="form-input" placeholder="e.g., A20EC0001 or L001"
32
+ style="text-transform: uppercase;">
33
+ <small class="text-muted" style="font-size: 0.875rem;">
34
+ Students: Start with A or B | Lecturers: Start with L
35
+ </small>
36
+ </div>
37
+
38
+ <div class="form-group">
39
+ <label class="form-label">Password</label>
40
+ <input type="password" id="password" class="form-input" placeholder="Enter your password">
41
+ </div>
42
+
43
+ <!-- Status Messages -->
44
+ <div id="statusMessage" class="mt-2"></div>
45
+
46
+ <!-- Login Button -->
47
+ <button id="loginBtn" class="btn btn-primary mt-2" style="width: 100%;">
48
+ 🔐 Login
49
+ </button>
50
+
51
+ <div class="text-center mt-2">
52
+ <p class="text-muted">
53
+ Don't have an account? <a href="/register" style="color: var(--color-primary);">Register
54
+ here</a>
55
+ </p>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ </section>
60
+
61
+ <!-- Loading Overlay -->
62
+ <div id="loadingOverlay" class="loading-overlay hidden">
63
+ <div class="text-center">
64
+ <div class="spinner"></div>
65
+ <p class="mt-2" id="loadingText">Logging in...</p>
66
+ </div>
67
+ </div>
68
+
69
+ <script>
70
+ // Handle Enter key
71
+ document.getElementById('password').addEventListener('keypress', (e) => {
72
+ if (e.key === 'Enter') {
73
+ document.getElementById('loginBtn').click();
74
+ }
75
+ });
76
+
77
+ // Login function
78
+ document.getElementById('loginBtn').addEventListener('click', async () => {
79
+ const matricNo = document.getElementById('matricNo').value.trim().toUpperCase();
80
+ const password = document.getElementById('password').value;
81
+
82
+ if (!matricNo || !password) {
83
+ showStatus('Please fill in all fields', 'error');
84
+ return;
85
+ }
86
+
87
+ showLoading('Logging in...');
88
+
89
+ try {
90
+ const response = await fetch('/api/auth/login', {
91
+ method: 'POST',
92
+ headers: { 'Content-Type': 'application/json' },
93
+ body: JSON.stringify({
94
+ matric_no: matricNo,
95
+ password: password
96
+ })
97
+ });
98
+
99
+ const data = await response.json();
100
+ hideLoading();
101
+
102
+ if (data.success) {
103
+ showStatus(data.message, 'success');
104
+
105
+ // Redirect after 1 second
106
+ setTimeout(() => {
107
+ window.location.href = data.data.redirect;
108
+ }, 1000);
109
+ } else {
110
+ showStatus(data.error, 'error');
111
+ }
112
+ } catch (error) {
113
+ hideLoading();
114
+ showStatus('Login failed. Please try again.', 'error');
115
+ }
116
+ });
117
+
118
+ function showLoading(text) {
119
+ document.getElementById('loadingText').textContent = text;
120
+ document.getElementById('loadingOverlay').classList.remove('hidden');
121
+ }
122
+
123
+ function hideLoading() {
124
+ document.getElementById('loadingOverlay').classList.add('hidden');
125
+ }
126
+
127
+ function showStatus(message, type) {
128
+ const statusDiv = document.getElementById('statusMessage');
129
+ const alertClass = type === 'success' ? 'alert-success' : 'alert-error';
130
+ statusDiv.innerHTML = `<div class="alert ${alertClass}">${message}</div>`;
131
+
132
+ if (type === 'error') {
133
+ setTimeout(() => {
134
+ statusDiv.innerHTML = '';
135
+ }, 5000);
136
+ }
137
+ }
138
+ </script>
139
+ </body>
140
+
141
+ </html>
models.py ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask_sqlalchemy import SQLAlchemy
2
+ from datetime import datetime
3
+ import json
4
+ from werkzeug.security import generate_password_hash, check_password_hash
5
+
6
+ db = SQLAlchemy()
7
+
8
+ class User(db.Model):
9
+ """User model for authentication - supports both students and lecturers"""
10
+ __tablename__ = 'users'
11
+
12
+ id = db.Column(db.Integer, primary_key=True)
13
+ matric_no = db.Column(db.String(50), unique=True, nullable=False) # A/B for students, L for lecturers
14
+ name = db.Column(db.String(100), nullable=False)
15
+ email = db.Column(db.String(100), unique=True)
16
+ password_hash = db.Column(db.String(255), nullable=False)
17
+ role = db.Column(db.String(20), nullable=False) # 'student' or 'lecturer'
18
+ is_active = db.Column(db.Boolean, default=True)
19
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
20
+
21
+ def set_password(self, password):
22
+ """Hash and set password"""
23
+ self.password_hash = generate_password_hash(password)
24
+
25
+ def check_password(self, password):
26
+ """Verify password"""
27
+ return check_password_hash(self.password_hash, password)
28
+
29
+ @staticmethod
30
+ def validate_matric_no(matric_no, role):
31
+ """Validate matric number format based on role"""
32
+ if not matric_no:
33
+ return False, "Matric number is required"
34
+
35
+ matric_no = matric_no.upper().strip()
36
+
37
+ if role == 'student':
38
+ # Students must start with A or B
39
+ if not (matric_no.startswith('A') or matric_no.startswith('B')):
40
+ return False, "Student matric number must start with 'A' or 'B'"
41
+ elif role == 'lecturer':
42
+ # Lecturers must start with L
43
+ if not matric_no.startswith('L'):
44
+ return False, "Lecturer matric number must start with 'L'"
45
+ else:
46
+ return False, "Invalid role"
47
+
48
+ return True, None
49
+
50
+ def to_dict(self):
51
+ return {
52
+ 'id': self.id,
53
+ 'matric_no': self.matric_no,
54
+ 'name': self.name,
55
+ 'email': self.email,
56
+ 'role': self.role,
57
+ 'is_active': self.is_active,
58
+ 'created_at': self.created_at.isoformat() if self.created_at else None
59
+ }
60
+
61
+
62
+ class Student(db.Model):
63
+ """Student model - stores student information and face encodings"""
64
+ __tablename__ = 'students'
65
+
66
+ id = db.Column(db.Integer, primary_key=True)
67
+ user_id = db.Column(db.Integer, db.ForeignKey('users.id'), unique=True) # Link to User
68
+ student_id = db.Column(db.String(50), unique=True, nullable=False)
69
+ name = db.Column(db.String(100), nullable=False)
70
+ email = db.Column(db.String(100), unique=True)
71
+ face_encoding = db.Column(db.Text) # Stored as JSON string
72
+ registered_at = db.Column(db.DateTime, default=datetime.utcnow)
73
+ is_active = db.Column(db.Boolean, default=True)
74
+
75
+ # Relationships
76
+ attendance_records = db.relationship('AttendanceRecord', backref='student', lazy=True)
77
+ user = db.relationship('User', backref='student_profile', uselist=False)
78
+
79
+ def get_face_encoding(self):
80
+ """Deserialize face encoding from JSON"""
81
+ if self.face_encoding:
82
+ return json.loads(self.face_encoding)
83
+ return None
84
+
85
+ def set_face_encoding(self, encoding):
86
+ """Serialize face encoding to JSON"""
87
+ if encoding is not None:
88
+ self.face_encoding = json.dumps(encoding.tolist())
89
+
90
+ def to_dict(self):
91
+ return {
92
+ 'id': self.id,
93
+ 'student_id': self.student_id,
94
+ 'name': self.name,
95
+ 'email': self.email,
96
+ 'registered_at': self.registered_at.isoformat() if self.registered_at else None,
97
+ 'is_active': self.is_active
98
+ }
99
+
100
+
101
+ class Classroom(db.Model):
102
+ """Classroom model - stores classroom locations with GPS coordinates"""
103
+ __tablename__ = 'classrooms'
104
+
105
+ id = db.Column(db.Integer, primary_key=True)
106
+ name = db.Column(db.String(100), nullable=False)
107
+ building = db.Column(db.String(100))
108
+ latitude = db.Column(db.Float, nullable=False)
109
+ longitude = db.Column(db.Float, nullable=False)
110
+ radius_meters = db.Column(db.Integer, default=50) # Geofencing radius
111
+
112
+ # Relationships
113
+ sessions = db.relationship('AttendanceSession', backref='classroom', lazy=True)
114
+
115
+ def to_dict(self):
116
+ return {
117
+ 'id': self.id,
118
+ 'name': self.name,
119
+ 'building': self.building,
120
+ 'latitude': self.latitude,
121
+ 'longitude': self.longitude,
122
+ 'radius_meters': self.radius_meters
123
+ }
124
+
125
+
126
+ class AttendanceSession(db.Model):
127
+ """Attendance session - represents a class session with auto-refresh codes"""
128
+ __tablename__ = 'attendance_sessions'
129
+
130
+ id = db.Column(db.Integer, primary_key=True)
131
+ course_name = db.Column(db.String(100), nullable=False)
132
+ classroom_id = db.Column(db.Integer, db.ForeignKey('classrooms.id'), nullable=False)
133
+ lecturer_name = db.Column(db.String(100), nullable=False)
134
+
135
+ # Session timing
136
+ start_time = db.Column(db.DateTime, default=datetime.utcnow)
137
+ end_time = db.Column(db.DateTime)
138
+ is_active = db.Column(db.Boolean, default=True)
139
+
140
+ # Auto-refresh attendance code
141
+ current_code = db.Column(db.String(10))
142
+ code_generated_at = db.Column(db.DateTime)
143
+
144
+ # Relationships
145
+ attendance_records = db.relationship('AttendanceRecord', backref='session', lazy=True)
146
+
147
+ def to_dict(self):
148
+ return {
149
+ 'id': self.id,
150
+ 'course_name': self.course_name,
151
+ 'classroom_id': self.classroom_id,
152
+ 'classroom': self.classroom.to_dict() if self.classroom else None,
153
+ 'lecturer_name': self.lecturer_name,
154
+ 'start_time': self.start_time.isoformat() if self.start_time else None,
155
+ 'end_time': self.end_time.isoformat() if self.end_time else None,
156
+ 'is_active': self.is_active,
157
+ 'current_code': self.current_code,
158
+ 'code_generated_at': self.code_generated_at.isoformat() if self.code_generated_at else None
159
+ }
160
+
161
+
162
+ class AttendanceRecord(db.Model):
163
+ """Attendance record - individual student attendance for a session"""
164
+ __tablename__ = 'attendance_records'
165
+
166
+ id = db.Column(db.Integer, primary_key=True)
167
+ session_id = db.Column(db.Integer, db.ForeignKey('attendance_sessions.id'), nullable=False)
168
+ student_id = db.Column(db.Integer, db.ForeignKey('students.id'), nullable=False)
169
+
170
+ # Verification details
171
+ marked_at = db.Column(db.DateTime, default=datetime.utcnow)
172
+ face_verified = db.Column(db.Boolean, default=False)
173
+ location_verified = db.Column(db.Boolean, default=False)
174
+ code_verified = db.Column(db.Boolean, default=False)
175
+
176
+ # Location data
177
+ student_latitude = db.Column(db.Float)
178
+ student_longitude = db.Column(db.Float)
179
+ distance_from_classroom = db.Column(db.Float) # In meters
180
+
181
+ # Status
182
+ status = db.Column(db.String(20), default='present') # present, late, absent
183
+
184
+ def to_dict(self):
185
+ return {
186
+ 'id': self.id,
187
+ 'session_id': self.session_id,
188
+ 'student': self.student.to_dict() if self.student else None,
189
+ 'marked_at': self.marked_at.isoformat() if self.marked_at else None,
190
+ 'face_verified': self.face_verified,
191
+ 'location_verified': self.location_verified,
192
+ 'code_verified': self.code_verified,
193
+ 'distance_from_classroom': self.distance_from_classroom,
194
+ 'status': self.status
195
+ }
register.html ADDED
@@ -0,0 +1,366 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Register - Attendr</title>
8
+ <link rel="stylesheet" href="style.css">
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@700;800&display=swap"
11
+ rel="stylesheet">
12
+ </head>
13
+
14
+ <body>
15
+ <!-- Navigation -->
16
+ <!-- Navigation -->
17
+ {% include '_navbar.html' %}
18
+
19
+ <!-- Main Content -->
20
+ <section class="section">
21
+ <div class="container" style="max-width: 800px;">
22
+ <h1 class="text-center mb-3">Register Account</h1>
23
+ <p class="text-center text-muted mb-3">
24
+ Create your student or lecturer account
25
+ </p>
26
+
27
+ <!-- Step 1: Account Details -->
28
+ <div id="step1Card" class="glass-card">
29
+ <h2 class="mb-2">Step 1: Account Information</h2>
30
+
31
+ <div class="form-group">
32
+ <label class="form-label">I am a...</label>
33
+ <select id="roleSelect" class="form-select">
34
+ <option value="">Select your role</option>
35
+ <option value="student">Student</option>
36
+ <option value="lecturer">Lecturer</option>
37
+ </select>
38
+ </div>
39
+
40
+ <div class="form-group">
41
+ <label class="form-label">Matric Number</label>
42
+ <input type="text" id="matricNo" class="form-input" placeholder="e.g., A20EC0001 or L001"
43
+ style="text-transform: uppercase;">
44
+ <small class="text-muted" id="matricHelp" style="font-size: 0.875rem;">
45
+ Select your role first
46
+ </small>
47
+ </div>
48
+
49
+ <div class="form-group">
50
+ <label class="form-label">Full Name</label>
51
+ <input type="text" id="fullName" class="form-input" placeholder="Your full name">
52
+ </div>
53
+
54
+ <div class="form-group">
55
+ <label class="form-label">Email (Optional)</label>
56
+ <input type="email" id="email" class="form-input" placeholder="your.email@graduate.utm.my">
57
+ </div>
58
+
59
+ <div class="form-group">
60
+ <label class="form-label">Password</label>
61
+ <input type="password" id="password" class="form-input" placeholder="Create a strong password">
62
+ </div>
63
+
64
+ <div class="form-group">
65
+ <label class="form-label">Confirm Password</label>
66
+ <input type="password" id="confirmPassword" class="form-input" placeholder="Re-enter your password">
67
+ </div>
68
+
69
+ <div id="statusMessage" class="mt-2"></div>
70
+
71
+ <button id="nextBtn" class="btn btn-primary mt-2" style="width: 100%;">
72
+ Next: Face Registration →
73
+ </button>
74
+
75
+ <div class="text-center mt-2">
76
+ <p class="text-muted">
77
+ Already have an account? <a href="/login" style="color: var(--color-primary);">Login here</a>
78
+ </p>
79
+ </div>
80
+ </div>
81
+
82
+ <!-- Step 2: Face Registration (Students Only) -->
83
+ <div id="step2Card" class="glass-card mt-2 hidden">
84
+ <h2 class="mb-2">Step 2: Face Registration</h2>
85
+ <p class="text-muted mb-2">Capture your face for biometric verification</p>
86
+
87
+ <div class="text-center">
88
+ <video id="video" width="400" height="300" autoplay
89
+ style="border-radius: 12px; border: 2px solid var(--color-primary);"></video>
90
+ <canvas id="canvas" width="400" height="300" style="display: none;"></canvas>
91
+
92
+ <div id="preview" class="mt-2 hidden">
93
+ <img id="capturedImage"
94
+ style="max-width: 400px; border-radius: 12px; border: 2px solid var(--color-success);">
95
+ </div>
96
+ </div>
97
+
98
+ <div id="faceStatusMessage" class="mt-2"></div>
99
+
100
+ <div class="flex-center gap-2 mt-2">
101
+ <button id="startCameraBtn" class="btn btn-secondary">
102
+ 📷 Start Camera
103
+ </button>
104
+ <button id="captureBtn" class="btn btn-primary hidden">
105
+ 📸 Capture Face
106
+ </button>
107
+ <button id="retakeBtn" class="btn btn-outline hidden">
108
+ 🔄 Retake
109
+ </button>
110
+ <button id="registerBtn" class="btn btn-success hidden">
111
+ ✅ Complete Registration
112
+ </button>
113
+ </div>
114
+
115
+ <div class="text-center mt-2">
116
+ <button id="backBtn" class="btn btn-outline">
117
+ ← Back to Account Details
118
+ </button>
119
+ </div>
120
+ </div>
121
+
122
+ <!-- Success Message -->
123
+ <div id="successCard" class="glass-card mt-2 hidden"
124
+ style="background: rgba(34, 197, 94, 0.1); border: 2px solid var(--color-success);">
125
+ <div class="text-center">
126
+ <div style="font-size: 4rem; margin-bottom: 1rem;">✅</div>
127
+ <h2 style="color: var(--color-success);">Registration Successful!</h2>
128
+ <p class="text-muted mt-2" id="successMessage">Your account has been created successfully.</p>
129
+ <div class="flex-center gap-2 mt-2">
130
+ <a href="/login" class="btn btn-primary">Go to Login</a>
131
+ </div>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ </section>
136
+
137
+ <!-- Loading Overlay -->
138
+ <div id="loadingOverlay" class="loading-overlay hidden">
139
+ <div class="text-center">
140
+ <div class="spinner"></div>
141
+ <p class="mt-2" id="loadingText">Processing...</p>
142
+ </div>
143
+ </div>
144
+
145
+ <script>
146
+ let videoStream = null;
147
+ let capturedImageData = null;
148
+ let userRole = null;
149
+
150
+ // Update matric number help text based on role
151
+ document.getElementById('roleSelect').addEventListener('change', (e) => {
152
+ userRole = e.target.value;
153
+ const helpText = document.getElementById('matricHelp');
154
+
155
+ if (userRole === 'student') {
156
+ helpText.textContent = 'Student matric number must start with A or B (e.g., A20EC0001)';
157
+ helpText.style.color = 'var(--color-primary)';
158
+ } else if (userRole === 'lecturer') {
159
+ helpText.textContent = 'Lecturer matric number must start with L (e.g., L001)';
160
+ helpText.style.color = 'var(--color-primary)';
161
+ } else {
162
+ helpText.textContent = 'Select your role first';
163
+ helpText.style.color = 'var(--color-text-muted)';
164
+ }
165
+ });
166
+
167
+ // Next button - validate and move to face registration
168
+ document.getElementById('nextBtn').addEventListener('click', async () => {
169
+ const role = document.getElementById('roleSelect').value;
170
+ const matricNo = document.getElementById('matricNo').value.trim().toUpperCase();
171
+ const fullName = document.getElementById('fullName').value.trim();
172
+ const password = document.getElementById('password').value;
173
+ const confirmPassword = document.getElementById('confirmPassword').value;
174
+
175
+ // Validation
176
+ if (!role) {
177
+ showStatus('Please select your role', 'error');
178
+ return;
179
+ }
180
+
181
+ if (!matricNo || !fullName || !password) {
182
+ showStatus('Please fill in all required fields', 'error');
183
+ return;
184
+ }
185
+
186
+ if (password !== confirmPassword) {
187
+ showStatus('Passwords do not match', 'error');
188
+ return;
189
+ }
190
+
191
+ if (password.length < 6) {
192
+ showStatus('Password must be at least 6 characters long', 'error');
193
+ return;
194
+ }
195
+
196
+ // If lecturer, register directly (no face needed)
197
+ if (role === 'lecturer') {
198
+ await registerUser(false);
199
+ } else {
200
+ // Student - go to face registration
201
+ document.getElementById('step1Card').classList.add('hidden');
202
+ document.getElementById('step2Card').classList.remove('hidden');
203
+ }
204
+ });
205
+
206
+ // Back button
207
+ document.getElementById('backBtn').addEventListener('click', () => {
208
+ stopCamera();
209
+ document.getElementById('step2Card').classList.add('hidden');
210
+ document.getElementById('step1Card').classList.remove('hidden');
211
+ });
212
+
213
+ // Camera controls
214
+ document.getElementById('startCameraBtn').addEventListener('click', startCamera);
215
+ document.getElementById('captureBtn').addEventListener('click', captureFace);
216
+ document.getElementById('retakeBtn').addEventListener('click', retake);
217
+ document.getElementById('registerBtn').addEventListener('click', () => registerUser(true));
218
+
219
+ async function startCamera() {
220
+ try {
221
+ videoStream = await navigator.mediaDevices.getUserMedia({ video: true });
222
+ document.getElementById('video').srcObject = videoStream;
223
+ document.getElementById('startCameraBtn').classList.add('hidden');
224
+ document.getElementById('captureBtn').classList.remove('hidden');
225
+ showFaceStatus('Camera ready! Position your face in the frame', 'success');
226
+ } catch (error) {
227
+ showFaceStatus('Camera access denied. Please allow camera access.', 'error');
228
+ }
229
+ }
230
+
231
+ function stopCamera() {
232
+ if (videoStream) {
233
+ videoStream.getTracks().forEach(track => track.stop());
234
+ videoStream = null;
235
+ }
236
+ }
237
+
238
+ function captureFace() {
239
+ const video = document.getElementById('video');
240
+ const canvas = document.getElementById('canvas');
241
+ const context = canvas.getContext('2d');
242
+
243
+ context.drawImage(video, 0, 0, 400, 300);
244
+ capturedImageData = canvas.toDataURL('image/jpeg');
245
+
246
+ document.getElementById('capturedImage').src = capturedImageData;
247
+ document.getElementById('preview').classList.remove('hidden');
248
+ document.getElementById('video').style.display = 'none';
249
+ document.getElementById('captureBtn').classList.add('hidden');
250
+ document.getElementById('retakeBtn').classList.remove('hidden');
251
+ document.getElementById('registerBtn').classList.remove('hidden');
252
+
253
+ stopCamera();
254
+ showFaceStatus('Face captured! Click "Complete Registration" to finish', 'success');
255
+ }
256
+
257
+ function retake() {
258
+ document.getElementById('preview').classList.add('hidden');
259
+ document.getElementById('video').style.display = 'block';
260
+ document.getElementById('retakeBtn').classList.add('hidden');
261
+ document.getElementById('registerBtn').classList.add('hidden');
262
+ capturedImageData = null;
263
+ startCamera();
264
+ }
265
+
266
+ async function registerUser(includeFace) {
267
+ const role = document.getElementById('roleSelect').value;
268
+ const matricNo = document.getElementById('matricNo').value.trim().toUpperCase();
269
+ const fullName = document.getElementById('fullName').value.trim();
270
+ const email = document.getElementById('email').value.trim();
271
+ const password = document.getElementById('password').value;
272
+
273
+ if (includeFace && !capturedImageData) {
274
+ showFaceStatus('Please capture your face first', 'error');
275
+ return;
276
+ }
277
+
278
+ showLoading('Creating your account...');
279
+
280
+ try {
281
+ // Step 1: Create user account
282
+ const userResponse = await fetch('/api/auth/register', {
283
+ method: 'POST',
284
+ headers: { 'Content-Type': 'application/json' },
285
+ body: JSON.stringify({
286
+ role: role,
287
+ matric_no: matricNo,
288
+ name: fullName,
289
+ email: email || null,
290
+ password: password
291
+ })
292
+ });
293
+
294
+ const userData = await userResponse.json();
295
+
296
+ if (!userData.success) {
297
+ hideLoading();
298
+ showStatus(userData.error, 'error');
299
+ return;
300
+ }
301
+
302
+ // Step 2: Register face if student
303
+ if (includeFace) {
304
+ const faceResponse = await fetch('/api/register_face', {
305
+ method: 'POST',
306
+ headers: { 'Content-Type': 'application/json' },
307
+ body: JSON.stringify({
308
+ student_id: matricNo,
309
+ name: fullName,
310
+ email: email || null,
311
+ image: capturedImageData
312
+ })
313
+ });
314
+
315
+ const faceData = await faceResponse.json();
316
+ hideLoading();
317
+
318
+ if (!faceData.success) {
319
+ showFaceStatus(faceData.error, 'error');
320
+ return;
321
+ }
322
+ }
323
+
324
+ hideLoading();
325
+
326
+ // Show success
327
+ document.getElementById('step1Card').classList.add('hidden');
328
+ document.getElementById('step2Card').classList.add('hidden');
329
+ document.getElementById('successCard').classList.remove('hidden');
330
+ document.getElementById('successMessage').textContent =
331
+ `Your ${role} account has been created successfully${includeFace ? ' with face registration' : ''}. You can now login to access the system.`;
332
+
333
+ } catch (error) {
334
+ hideLoading();
335
+ showStatus('Registration failed. Please try again.', 'error');
336
+ }
337
+ }
338
+
339
+ function showLoading(text) {
340
+ document.getElementById('loadingText').textContent = text;
341
+ document.getElementById('loadingOverlay').classList.remove('hidden');
342
+ }
343
+
344
+ function hideLoading() {
345
+ document.getElementById('loadingOverlay').classList.add('hidden');
346
+ }
347
+
348
+ function showStatus(message, type) {
349
+ const statusDiv = document.getElementById('statusMessage');
350
+ const alertClass = type === 'success' ? 'alert-success' : 'alert-error';
351
+ statusDiv.innerHTML = `<div class="alert ${alertClass}">${message}</div>`;
352
+
353
+ setTimeout(() => {
354
+ statusDiv.innerHTML = '';
355
+ }, 5000);
356
+ }
357
+
358
+ function showFaceStatus(message, type) {
359
+ const statusDiv = document.getElementById('faceStatusMessage');
360
+ const alertClass = type === 'success' ? 'alert-success' : 'alert-error';
361
+ statusDiv.innerHTML = `<div class="alert ${alertClass}">${message}</div>`;
362
+ }
363
+ </script>
364
+ </body>
365
+
366
+ </html>
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ Flask==3.0.0
2
+ Flask-SQLAlchemy==3.1.1
3
+ opencv-python==4.8.1.78
4
+ face-recognition==1.3.0
5
+ numpy==1.24.3
6
+ Pillow==10.1.0
7
+ python-dotenv==1.0.0
8
+ werkzeug==3.0.1
student.html ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Student Portal - Attendr</title>
8
+ <link rel="stylesheet" href="style.css">
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@700;800&display=swap"
11
+ rel="stylesheet">
12
+ </head>
13
+
14
+ <body>
15
+ <!-- Navigation -->
16
+ <!-- Navigation -->
17
+ {% include '_navbar.html' %}
18
+
19
+ <!-- Main Content -->
20
+ <section class="section">
21
+ <div class="container" style="max-width: 800px;">
22
+ <h1 class="text-center mb-3">Student Attendance Portal</h1>
23
+
24
+ <!-- Step 1: Student ID Input -->
25
+ <div id="step1" class="glass-card mb-2">
26
+ <h2>Step 1: Enter Student ID</h2>
27
+ <div class="form-group">
28
+ <label class="form-label">Student ID</label>
29
+ <input type="text" id="studentId" class="form-input" placeholder="e.g., A20EC0001">
30
+ </div>
31
+ <div class="form-group">
32
+ <label class="form-label">Select Session</label>
33
+ <select id="sessionSelect" class="form-select">
34
+ <option value="">Loading sessions...</option>
35
+ </select>
36
+ </div>
37
+ <button id="startBtn" class="btn btn-primary" disabled>Start Verification</button>
38
+ </div>
39
+
40
+ <!-- Step 2: Device Permissions -->
41
+ <div id="step2" class="glass-card mb-2 hidden">
42
+ <h2>Step 2: Enable Permissions</h2>
43
+ <p class="text-muted">We need access to your camera and location to verify your attendance.</p>
44
+
45
+ <div class="grid grid-2 gap-2 mt-2">
46
+ <div class="status-badge status-info" id="cameraStatus">
47
+ <span>📷</span>
48
+ <span>Camera: Waiting...</span>
49
+ </div>
50
+ <div class="status-badge status-info" id="gpsStatus">
51
+ <span>📍</span>
52
+ <span>GPS: Waiting...</span>
53
+ </div>
54
+ </div>
55
+
56
+ <button id="enablePermissionsBtn" class="btn btn-primary mt-2">Enable Camera & GPS</button>
57
+ </div>
58
+
59
+ <!-- Step 3: Face Verification -->
60
+ <div id="step3" class="glass-card mb-2 hidden">
61
+ <h2>Step 3: Face Verification</h2>
62
+ <p class="text-muted">Position your face in the frame for verification.</p>
63
+
64
+ <div class="video-container mt-2">
65
+ <video id="videoPreview" class="video-preview" autoplay playsinline></video>
66
+ <canvas id="canvas" style="display: none;"></canvas>
67
+ </div>
68
+
69
+ <div id="faceStatus" class="mt-2"></div>
70
+
71
+ <button id="verifyFaceBtn" class="btn btn-primary mt-2">Verify My Face</button>
72
+ </div>
73
+
74
+ <!-- Step 4: Location Verification -->
75
+ <div id="step4" class="glass-card mb-2 hidden">
76
+ <h2>Step 4: Location Verification</h2>
77
+ <p class="text-muted">Verifying you are in the classroom...</p>
78
+
79
+ <div id="locationStatus" class="mt-2"></div>
80
+
81
+ <button id="verifyLocationBtn" class="btn btn-primary mt-2">Verify Location</button>
82
+ </div>
83
+
84
+ <!-- Step 5: Attendance Code -->
85
+ <div id="step5" class="glass-card mb-2 hidden">
86
+ <h2>Step 5: Enter Attendance Code</h2>
87
+ <p class="text-muted">Enter the code displayed by your lecturer.</p>
88
+
89
+ <div class="form-group mt-2">
90
+ <label class="form-label">Attendance Code</label>
91
+ <input type="text" id="attendanceCode" class="form-input" placeholder="Enter 6-digit code"
92
+ maxlength="6"
93
+ style="text-transform: uppercase; font-size: 1.5rem; text-align: center; letter-spacing: 0.5rem;">
94
+ </div>
95
+
96
+ <button id="submitAttendanceBtn" class="btn btn-success mt-2">Mark Attendance</button>
97
+ </div>
98
+
99
+ <!-- Success Message -->
100
+ <div id="successMessage" class="glass-card hidden"
101
+ style="background: rgba(34, 197, 94, 0.1); border: 2px solid var(--color-success);">
102
+ <div class="text-center">
103
+ <div style="font-size: 4rem; margin-bottom: 1rem;">✅</div>
104
+ <h2 style="color: var(--color-success);">Attendance Marked Successfully!</h2>
105
+ <p id="successDetails" class="text-muted mt-2"></p>
106
+ <button onclick="location.reload()" class="btn btn-primary mt-2">Mark Another Attendance</button>
107
+ </div>
108
+ </div>
109
+
110
+ <!-- Error Display -->
111
+ <div id="errorDisplay" class="hidden"></div>
112
+ </div>
113
+ </section>
114
+
115
+ <!-- Loading Overlay -->
116
+ <div id="loadingOverlay" class="loading-overlay hidden">
117
+ <div class="text-center">
118
+ <div class="spinner"></div>
119
+ <p class="mt-2" id="loadingText">Processing...</p>
120
+ </div>
121
+ </div>
122
+
123
+ <script src="{{ url_for('static', filename='js/student.js') }}"></script>
124
+ <script>
125
+ async function logout() {
126
+ if (!confirm('Are you sure you want to logout?')) return;
127
+
128
+ try {
129
+ const response = await fetch('/api/auth/logout', {
130
+ method: 'POST',
131
+ headers: { 'Content-Type': 'application/json' }
132
+ });
133
+
134
+ const data = await response.json();
135
+
136
+ if (data.success) {
137
+ alert(data.message);
138
+ window.location.href = '/';
139
+ } else {
140
+ alert('Logout failed');
141
+ }
142
+ } catch (error) {
143
+ alert('Logout failed');
144
+ }
145
+ }
146
+ </script>
147
+ </body>
148
+
149
+ </html>
student_history.html ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Attendance History - Attendr</title>
8
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@700;800&display=swap"
11
+ rel="stylesheet">
12
+ </head>
13
+
14
+ <body>
15
+ <!-- Navigation -->
16
+ <!-- Navigation -->
17
+ {% include '_navbar.html' %}
18
+
19
+ <!-- Main Content -->
20
+ <section class="section">
21
+ <div class="container">
22
+ <h1 class="text-center mb-3">📚 My Attendance History</h1>
23
+
24
+ <!-- Student Info Card -->
25
+ <div class="glass-card mb-2">
26
+ <div class="flex-between">
27
+ <div>
28
+ <h3 id="studentName">Loading...</h3>
29
+ <p class="text-muted" id="studentId"></p>
30
+ </div>
31
+ <div class="text-right">
32
+ <div style="font-size: 3rem; color: var(--color-primary);" id="totalCount">0</div>
33
+ <p class="text-muted">Total Attendance</p>
34
+ </div>
35
+ </div>
36
+ </div>
37
+
38
+ <!-- Attendance Records -->
39
+ <div id="historyContainer">
40
+ <div class="text-center mt-3">
41
+ <div class="spinner"></div>
42
+ <p class="text-muted mt-2">Loading your attendance history...</p>
43
+ </div>
44
+ </div>
45
+
46
+ <!-- Empty State -->
47
+ <div id="emptyState" class="glass-card text-center hidden" style="padding: 3rem;">
48
+ <div style="font-size: 4rem; margin-bottom: 1rem;">📭</div>
49
+ <h2>No Attendance Records Yet</h2>
50
+ <p class="text-muted mt-2">You haven't marked any attendance yet. Go to the student portal to mark your
51
+ first attendance!</p>
52
+ <a href="/student" class="btn btn-primary mt-2">Mark Attendance</a>
53
+ </div>
54
+ </div>
55
+ </section>
56
+
57
+ <script>
58
+ document.addEventListener('DOMContentLoaded', loadHistory);
59
+
60
+ async function loadHistory() {
61
+ try {
62
+ const response = await fetch('/api/student/history');
63
+ const data = await response.json();
64
+
65
+ if (data.success) {
66
+ displayHistory(data.data);
67
+ } else {
68
+ showError(data.error);
69
+ }
70
+ } catch (error) {
71
+ showError('Failed to load attendance history');
72
+ }
73
+ }
74
+
75
+ function displayHistory(data) {
76
+ document.getElementById('studentName').textContent = data.student.name;
77
+ document.getElementById('studentId').textContent = `Matric: ${data.student.student_id}`;
78
+ document.getElementById('totalCount').textContent = data.total_records;
79
+
80
+ const container = document.getElementById('historyContainer');
81
+
82
+ if (data.history.length === 0) {
83
+ container.innerHTML = '';
84
+ document.getElementById('emptyState').classList.remove('hidden');
85
+ return;
86
+ }
87
+
88
+ let html = '<div class="grid grid-1 gap-2">';
89
+
90
+ data.history.forEach(record => {
91
+ const date = new Date(record.marked_at);
92
+ const sessionDate = new Date(record.session_date);
93
+
94
+ html += `
95
+ <div class="glass-card">
96
+ <div class="flex-between mb-1">
97
+ <div>
98
+ <h3>${record.course_name}</h3>
99
+ <p class="text-muted">Lecturer: ${record.lecturer_name}</p>
100
+ </div>
101
+ <div class="badge badge-success">
102
+ ✓ ${record.status.toUpperCase()}
103
+ </div>
104
+ </div>
105
+
106
+ <div class="grid grid-2 gap-1 mt-2" style="font-size: 0.9rem;">
107
+ <div>
108
+ <strong>📍 Location:</strong><br>
109
+ ${record.classroom} - ${record.building}
110
+ </div>
111
+ <div>
112
+ <strong>📅 Date:</strong><br>
113
+ ${sessionDate.toLocaleDateString()} ${sessionDate.toLocaleTimeString()}
114
+ </div>
115
+ <div>
116
+ <strong>⏰ Marked At:</strong><br>
117
+ ${date.toLocaleDateString()} ${date.toLocaleTimeString()}
118
+ </div>
119
+ <div>
120
+ <strong>📏 Distance:</strong><br>
121
+ ${record.distance_from_classroom ? record.distance_from_classroom + 'm' : 'N/A'}
122
+ </div>
123
+ </div>
124
+
125
+ <div class="flex-center gap-1 mt-2">
126
+ <span class="badge ${record.face_verified ? 'badge-success' : 'badge-error'}">
127
+ ${record.face_verified ? '✓' : '✗'} Face
128
+ </span>
129
+ <span class="badge ${record.location_verified ? 'badge-success' : 'badge-error'}">
130
+ ${record.location_verified ? '✓' : '✗'} Location
131
+ </span>
132
+ <span class="badge ${record.code_verified ? 'badge-success' : 'badge-error'}">
133
+ ${record.code_verified ? '✓' : '✗'} Code
134
+ </span>
135
+ </div>
136
+ </div>
137
+ `;
138
+ });
139
+
140
+ html += '</div>';
141
+ container.innerHTML = html;
142
+ }
143
+
144
+ function showError(message) {
145
+ const container = document.getElementById('historyContainer');
146
+ container.innerHTML = `
147
+ <div class="alert alert-error">
148
+ ${message}
149
+ </div>
150
+ `;
151
+ }
152
+
153
+ async function logout() {
154
+ if (!confirm('Are you sure you want to logout?')) return;
155
+
156
+ try {
157
+ const response = await fetch('/api/auth/logout', {
158
+ method: 'POST',
159
+ headers: { 'Content-Type': 'application/json' }
160
+ });
161
+
162
+ const data = await response.json();
163
+
164
+ if (data.success) {
165
+ alert(data.message);
166
+ window.location.href = '/';
167
+ } else {
168
+ alert('Logout failed');
169
+ }
170
+ } catch (error) {
171
+ alert('Logout failed');
172
+ }
173
+ }
174
+ </script>
175
+ </body>
176
+
177
+ </html>
style.css ADDED
@@ -0,0 +1,593 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ * Attendr - UTM Smart Attendance System
3
+ * Design Theme: UTM Universiti Teknologi Malaysia
4
+ * Primary Colors: Maroon (#8B1538) and Gold (#F4A900)
5
+ */
6
+
7
+ /* ==================== IMPORTS ==================== */
8
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@700;800&display=swap');
9
+
10
+ /* ==================== CSS VARIABLES ==================== */
11
+ :root {
12
+ /* UTM Brand Colors */
13
+ --color-utm-maroon: #8B1538;
14
+ --color-utm-maroon-dark: #6B0F2A;
15
+ --color-utm-maroon-light: #A01D45;
16
+ --color-utm-gold: #F4A900;
17
+ --color-utm-gold-dark: #D49000;
18
+ --color-utm-gold-light: #FFB820;
19
+
20
+ /* Semantic Colors */
21
+ --color-primary: var(--color-utm-maroon);
22
+ --color-primary-dark: var(--color-utm-maroon-dark);
23
+ --color-primary-light: var(--color-utm-maroon-light);
24
+ --color-accent: var(--color-utm-gold);
25
+ --color-accent-dark: var(--color-utm-gold-dark);
26
+ --color-accent-light: var(--color-utm-gold-light);
27
+
28
+ /* UI Colors */
29
+ --color-success: #10B981;
30
+ --color-error: #EF4444;
31
+ --color-warning: #F59E0B;
32
+ --color-info: #3B82F6;
33
+
34
+ /* Neutral Colors */
35
+ --color-bg: #0F0A15;
36
+ --color-bg-secondary: #1A1425;
37
+ --color-surface: rgba(139, 21, 56, 0.1);
38
+ --color-surface-hover: rgba(139, 21, 56, 0.2);
39
+ --color-text: #F9FAFB;
40
+ --color-text-muted: #D1D5DB;
41
+ --color-border: rgba(244, 169, 0, 0.2);
42
+
43
+ /* Spacing */
44
+ --spacing-xs: 0.25rem;
45
+ --spacing-sm: 0.5rem;
46
+ --spacing-md: 1rem;
47
+ --spacing-lg: 1.5rem;
48
+ --spacing-xl: 2rem;
49
+ --spacing-2xl: 3rem;
50
+
51
+ /* Border Radius */
52
+ --radius-sm: 0.375rem;
53
+ --radius-md: 0.5rem;
54
+ --radius-lg: 0.75rem;
55
+ --radius-xl: 1rem;
56
+
57
+ /* Shadows */
58
+ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
59
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
60
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
61
+ --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
62
+
63
+ /* Transitions */
64
+ --transition-fast: 150ms ease-in-out;
65
+ --transition-base: 300ms ease-in-out;
66
+ --transition-slow: 500ms ease-in-out;
67
+ }
68
+
69
+ /* ==================== RESET & BASE ==================== */
70
+ * {
71
+ margin: 0;
72
+ padding: 0;
73
+ box-sizing: border-box;
74
+ }
75
+
76
+ body {
77
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
78
+ background: linear-gradient(135deg, #0F0A15 0%, #1A1425 50%, #0F0A15 100%);
79
+ background-attachment: fixed;
80
+ color: var(--color-text);
81
+ line-height: 1.6;
82
+ min-height: 100vh;
83
+ overflow-x: hidden;
84
+ }
85
+
86
+ /* UTM Pattern Background */
87
+ body::before {
88
+ content: '';
89
+ position: fixed;
90
+ top: 0;
91
+ left: 0;
92
+ width: 100%;
93
+ height: 100%;
94
+ background-image:
95
+ radial-gradient(circle at 20% 50%, rgba(139, 21, 56, 0.1) 0%, transparent 50%),
96
+ radial-gradient(circle at 80% 80%, rgba(244, 169, 0, 0.1) 0%, transparent 50%);
97
+ pointer-events: none;
98
+ z-index: 0;
99
+ }
100
+
101
+ /* ==================== TYPOGRAPHY ==================== */
102
+ h1,
103
+ h2,
104
+ h3,
105
+ h4,
106
+ h5,
107
+ h6 {
108
+ font-family: 'Outfit', 'Inter', sans-serif;
109
+ font-weight: 700;
110
+ line-height: 1.2;
111
+ color: var(--color-text);
112
+ }
113
+
114
+ h1 {
115
+ font-size: 2.5rem;
116
+ background: linear-gradient(135deg, var(--color-utm-maroon) 0%, var(--color-utm-gold) 100%);
117
+ -webkit-background-clip: text;
118
+ -webkit-text-fill-color: transparent;
119
+ background-clip: text;
120
+ }
121
+
122
+ h2 {
123
+ font-size: 2rem;
124
+ }
125
+
126
+ h3 {
127
+ font-size: 1.5rem;
128
+ }
129
+
130
+ /* ==================== LAYOUT ==================== */
131
+ .container {
132
+ max-width: 1200px;
133
+ margin: 0 auto;
134
+ padding: 0 var(--spacing-lg);
135
+ position: relative;
136
+ z-index: 1;
137
+ }
138
+
139
+ .section {
140
+ padding: var(--spacing-2xl) 0;
141
+ position: relative;
142
+ }
143
+
144
+ /* ==================== NAVIGATION ==================== */
145
+ .navbar {
146
+ background: rgba(15, 10, 21, 0.95);
147
+ backdrop-filter: blur(10px);
148
+ border-bottom: 2px solid var(--color-utm-gold);
149
+ padding: var(--spacing-md) 0;
150
+ position: sticky;
151
+ top: 0;
152
+ z-index: 1000;
153
+ box-shadow: 0 4px 20px rgba(139, 21, 56, 0.3);
154
+ }
155
+
156
+ .navbar-content {
157
+ display: flex;
158
+ justify-content: space-between;
159
+ align-items: center;
160
+ }
161
+
162
+ .navbar-brand {
163
+ font-family: 'Outfit', sans-serif;
164
+ font-size: 1.75rem;
165
+ font-weight: 800;
166
+ background: linear-gradient(135deg, var(--color-utm-maroon) 0%, var(--color-utm-gold) 100%);
167
+ -webkit-background-clip: text;
168
+ -webkit-text-fill-color: transparent;
169
+ background-clip: text;
170
+ text-decoration: none;
171
+ transition: transform var(--transition-fast);
172
+ }
173
+
174
+ .navbar-brand:hover {
175
+ transform: scale(1.05);
176
+ }
177
+
178
+ .navbar-nav {
179
+ display: flex;
180
+ list-style: none;
181
+ gap: var(--spacing-lg);
182
+ align-items: center;
183
+ }
184
+
185
+ .navbar-link {
186
+ color: var(--color-text);
187
+ text-decoration: none;
188
+ font-weight: 500;
189
+ padding: var(--spacing-sm) var(--spacing-md);
190
+ border-radius: var(--radius-md);
191
+ transition: all var(--transition-fast);
192
+ position: relative;
193
+ }
194
+
195
+ .navbar-link::after {
196
+ content: '';
197
+ position: absolute;
198
+ bottom: 0;
199
+ left: 50%;
200
+ transform: translateX(-50%);
201
+ width: 0;
202
+ height: 2px;
203
+ background: var(--color-utm-gold);
204
+ transition: width var(--transition-fast);
205
+ }
206
+
207
+ .navbar-link:hover {
208
+ color: var(--color-utm-gold);
209
+ }
210
+
211
+ .navbar-link:hover::after {
212
+ width: 80%;
213
+ }
214
+
215
+ /* ==================== GLASS CARD ==================== */
216
+ .glass-card {
217
+ background: rgba(26, 20, 37, 0.7);
218
+ backdrop-filter: blur(10px);
219
+ border: 1px solid var(--color-border);
220
+ border-radius: var(--radius-xl);
221
+ padding: var(--spacing-xl);
222
+ box-shadow:
223
+ 0 8px 32px rgba(139, 21, 56, 0.2),
224
+ inset 0 1px 0 rgba(244, 169, 0, 0.1);
225
+ transition: all var(--transition-base);
226
+ }
227
+
228
+ .glass-card:hover {
229
+ transform: translateY(-2px);
230
+ box-shadow:
231
+ 0 12px 40px rgba(139, 21, 56, 0.3),
232
+ inset 0 1px 0 rgba(244, 169, 0, 0.2);
233
+ border-color: rgba(244, 169, 0, 0.4);
234
+ }
235
+
236
+ /* ==================== BUTTONS ==================== */
237
+ .btn {
238
+ display: inline-block;
239
+ padding: 0.75rem 1.5rem;
240
+ font-size: 1rem;
241
+ font-weight: 600;
242
+ text-align: center;
243
+ text-decoration: none;
244
+ border: none;
245
+ border-radius: var(--radius-md);
246
+ cursor: pointer;
247
+ transition: all var(--transition-fast);
248
+ position: relative;
249
+ overflow: hidden;
250
+ }
251
+
252
+ .btn::before {
253
+ content: '';
254
+ position: absolute;
255
+ top: 50%;
256
+ left: 50%;
257
+ width: 0;
258
+ height: 0;
259
+ border-radius: 50%;
260
+ background: rgba(255, 255, 255, 0.1);
261
+ transform: translate(-50%, -50%);
262
+ transition: width 0.6s, height 0.6s;
263
+ }
264
+
265
+ .btn:hover::before {
266
+ width: 300px;
267
+ height: 300px;
268
+ }
269
+
270
+ .btn-primary {
271
+ background: linear-gradient(135deg, var(--color-utm-maroon) 0%, var(--color-utm-maroon-dark) 100%);
272
+ color: white;
273
+ box-shadow: 0 4px 15px rgba(139, 21, 56, 0.4);
274
+ }
275
+
276
+ .btn-primary:hover {
277
+ background: linear-gradient(135deg, var(--color-utm-maroon-light) 0%, var(--color-utm-maroon) 100%);
278
+ box-shadow: 0 6px 20px rgba(139, 21, 56, 0.6);
279
+ transform: translateY(-2px);
280
+ }
281
+
282
+ .btn-secondary {
283
+ background: linear-gradient(135deg, var(--color-utm-gold) 0%, var(--color-utm-gold-dark) 100%);
284
+ color: var(--color-utm-maroon-dark);
285
+ box-shadow: 0 4px 15px rgba(244, 169, 0, 0.4);
286
+ }
287
+
288
+ .btn-secondary:hover {
289
+ background: linear-gradient(135deg, var(--color-utm-gold-light) 0%, var(--color-utm-gold) 100%);
290
+ box-shadow: 0 6px 20px rgba(244, 169, 0, 0.6);
291
+ transform: translateY(-2px);
292
+ }
293
+
294
+ .btn-success {
295
+ background: linear-gradient(135deg, var(--color-success) 0%, #059669 100%);
296
+ color: white;
297
+ box-shadow: 0 4px 15px rgba(16, 185, 129, 0.4);
298
+ }
299
+
300
+ .btn-success:hover {
301
+ background: linear-gradient(135deg, #10B981 0%, var(--color-success) 100%);
302
+ transform: translateY(-2px);
303
+ }
304
+
305
+ .btn-outline {
306
+ background: transparent;
307
+ color: var(--color-utm-gold);
308
+ border: 2px solid var(--color-utm-gold);
309
+ }
310
+
311
+ .btn-outline:hover {
312
+ background: var(--color-utm-gold);
313
+ color: var(--color-utm-maroon-dark);
314
+ }
315
+
316
+ .btn:disabled {
317
+ opacity: 0.5;
318
+ cursor: not-allowed;
319
+ }
320
+
321
+ /* ==================== FORMS ==================== */
322
+ .form-group {
323
+ margin-bottom: var(--spacing-lg);
324
+ }
325
+
326
+ .form-label {
327
+ display: block;
328
+ margin-bottom: var(--spacing-sm);
329
+ font-weight: 600;
330
+ color: var(--color-utm-gold);
331
+ }
332
+
333
+ .form-input,
334
+ .form-select {
335
+ width: 100%;
336
+ padding: 0.75rem 1rem;
337
+ background: rgba(26, 20, 37, 0.6);
338
+ border: 1px solid var(--color-border);
339
+ border-radius: var(--radius-md);
340
+ color: var(--color-text);
341
+ font-size: 1rem;
342
+ transition: all var(--transition-fast);
343
+ }
344
+
345
+ .form-input:focus,
346
+ .form-select:focus {
347
+ outline: none;
348
+ border-color: var(--color-utm-gold);
349
+ box-shadow: 0 0 0 3px rgba(244, 169, 0, 0.1);
350
+ background: rgba(26, 20, 37, 0.8);
351
+ }
352
+
353
+ .form-select option {
354
+ background: #1A1425;
355
+ color: var(--color-text);
356
+ }
357
+
358
+ /* ==================== BADGES ==================== */
359
+ .badge {
360
+ display: inline-block;
361
+ padding: 0.25rem 0.75rem;
362
+ font-size: 0.875rem;
363
+ font-weight: 600;
364
+ border-radius: var(--radius-sm);
365
+ background: rgba(139, 21, 56, 0.2);
366
+ color: var(--color-utm-gold);
367
+ border: 1px solid var(--color-utm-gold);
368
+ }
369
+
370
+ .badge-success {
371
+ background: rgba(16, 185, 129, 0.2);
372
+ color: var(--color-success);
373
+ border-color: var(--color-success);
374
+ }
375
+
376
+ .badge-error {
377
+ background: rgba(239, 68, 68, 0.2);
378
+ color: var(--color-error);
379
+ border-color: var(--color-error);
380
+ }
381
+
382
+ /* ==================== CODE DISPLAY ==================== */
383
+ .code-display {
384
+ font-family: 'Courier New', monospace;
385
+ font-size: 4rem;
386
+ font-weight: 800;
387
+ text-align: center;
388
+ padding: var(--spacing-xl);
389
+ background: linear-gradient(135deg, var(--color-utm-maroon) 0%, var(--color-utm-gold) 100%);
390
+ -webkit-background-clip: text;
391
+ -webkit-text-fill-color: transparent;
392
+ background-clip: text;
393
+ letter-spacing: 1rem;
394
+ text-shadow: 0 0 30px rgba(244, 169, 0, 0.5);
395
+ }
396
+
397
+ /* ==================== UTILITIES ==================== */
398
+ .text-center {
399
+ text-align: center;
400
+ }
401
+
402
+ .text-right {
403
+ text-align: right;
404
+ }
405
+
406
+ .text-muted {
407
+ color: var(--color-text-muted);
408
+ }
409
+
410
+ .mt-1 {
411
+ margin-top: var(--spacing-sm);
412
+ }
413
+
414
+ .mt-2 {
415
+ margin-top: var(--spacing-md);
416
+ }
417
+
418
+ .mt-3 {
419
+ margin-top: var(--spacing-lg);
420
+ }
421
+
422
+ .mb-1 {
423
+ margin-bottom: var(--spacing-sm);
424
+ }
425
+
426
+ .mb-2 {
427
+ margin-bottom: var(--spacing-md);
428
+ }
429
+
430
+ .mb-3 {
431
+ margin-bottom: var(--spacing-lg);
432
+ }
433
+
434
+ .hidden {
435
+ display: none !important;
436
+ }
437
+
438
+ .grid {
439
+ display: grid;
440
+ gap: var(--spacing-lg);
441
+ }
442
+
443
+ .grid-1 {
444
+ grid-template-columns: 1fr;
445
+ }
446
+
447
+ .grid-2 {
448
+ grid-template-columns: repeat(2, 1fr);
449
+ }
450
+
451
+ .grid-3 {
452
+ grid-template-columns: repeat(3, 1fr);
453
+ }
454
+
455
+ .gap-1 {
456
+ gap: var(--spacing-sm);
457
+ }
458
+
459
+ .gap-2 {
460
+ gap: var(--spacing-md);
461
+ }
462
+
463
+ .flex-between {
464
+ display: flex;
465
+ justify-content: space-between;
466
+ align-items: center;
467
+ }
468
+
469
+ .flex-center {
470
+ display: flex;
471
+ justify-content: center;
472
+ align-items: center;
473
+ }
474
+
475
+ /* ==================== LOADING & OVERLAYS ==================== */
476
+ .loading-overlay {
477
+ position: fixed;
478
+ top: 0;
479
+ left: 0;
480
+ width: 100%;
481
+ height: 100%;
482
+ background: rgba(15, 10, 21, 0.9);
483
+ display: flex;
484
+ justify-content: center;
485
+ align-items: center;
486
+ z-index: 9999;
487
+ }
488
+
489
+ .spinner {
490
+ width: 50px;
491
+ height: 50px;
492
+ border: 4px solid rgba(244, 169, 0, 0.2);
493
+ border-top-color: var(--color-utm-gold);
494
+ border-radius: 50%;
495
+ animation: spin 1s linear infinite;
496
+ }
497
+
498
+ @keyframes spin {
499
+ to {
500
+ transform: rotate(360deg);
501
+ }
502
+ }
503
+
504
+ /* ==================== ALERTS ==================== */
505
+ .alert {
506
+ padding: var(--spacing-md);
507
+ border-radius: var(--radius-md);
508
+ margin-bottom: var(--spacing-md);
509
+ border-left: 4px solid;
510
+ }
511
+
512
+ .alert-error {
513
+ background: rgba(239, 68, 68, 0.1);
514
+ border-color: var(--color-error);
515
+ color: #FCA5A5;
516
+ }
517
+
518
+ .alert-success {
519
+ background: rgba(16, 185, 129, 0.1);
520
+ border-color: var(--color-success);
521
+ color: #6EE7B7;
522
+ }
523
+
524
+ /* ==================== VIDEO & MEDIA ==================== */
525
+ .video-container {
526
+ position: relative;
527
+ border-radius: var(--radius-lg);
528
+ overflow: hidden;
529
+ border: 2px solid var(--color-utm-gold);
530
+ }
531
+
532
+ .video-preview {
533
+ width: 100%;
534
+ height: auto;
535
+ display: block;
536
+ }
537
+
538
+ /* ==================== STATUS BADGES ==================== */
539
+ .status-badge {
540
+ padding: var(--spacing-md);
541
+ border-radius: var(--radius-md);
542
+ border: 1px solid var(--color-border);
543
+ background: rgba(26, 20, 37, 0.6);
544
+ }
545
+
546
+ .status-info {
547
+ border-color: var(--color-utm-gold);
548
+ background: rgba(244, 169, 0, 0.1);
549
+ }
550
+
551
+ /* ==================== RESPONSIVE ==================== */
552
+ @media (max-width: 768px) {
553
+
554
+ .grid-2,
555
+ .grid-3 {
556
+ grid-template-columns: 1fr;
557
+ }
558
+
559
+ .navbar-nav {
560
+ gap: var(--spacing-sm);
561
+ font-size: 0.875rem;
562
+ }
563
+
564
+ h1 {
565
+ font-size: 2rem;
566
+ }
567
+
568
+ h2 {
569
+ font-size: 1.5rem;
570
+ }
571
+
572
+ .code-display {
573
+ font-size: 2.5rem;
574
+ letter-spacing: 0.5rem;
575
+ }
576
+ }
577
+
578
+ /* ==================== ANIMATIONS ==================== */
579
+ @keyframes fadeIn {
580
+ from {
581
+ opacity: 0;
582
+ transform: translateY(20px);
583
+ }
584
+
585
+ to {
586
+ opacity: 1;
587
+ transform: translateY(0);
588
+ }
589
+ }
590
+
591
+ .glass-card {
592
+ animation: fadeIn 0.5s ease-out;
593
+ }
update_utm_coordinates.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Update all UTM Faculty of Computing venues to correct GPS coordinates
3
+ Based on Plus Code: HJ7Q+F5 Johor Bahru (UTM Skudai Campus)
4
+ Coordinates: 1.5638°N, 103.6388°E
5
+ """
6
+
7
+ from app import app, db
8
+ from models import Classroom
9
+
10
+ def update_utm_coordinates():
11
+ """Update all UTM venues to correct coordinates, except PUTERI-COURT"""
12
+
13
+ with app.app_context():
14
+ print("Updating UTM Faculty of Computing venue coordinates...")
15
+ print("Base location: HJ7Q+F5 Johor Bahru (UTM Skudai)")
16
+ print("=" * 70)
17
+
18
+ # Base coordinates for UTM FC (from Plus Code HJ7Q+F5)
19
+ BASE_LAT = 1.5638
20
+ BASE_LON = 103.6388
21
+
22
+ # Get all venues except PUTERI-COURT
23
+ utm_venues = Classroom.query.filter(Classroom.name != 'PUTERI-COURT').all()
24
+
25
+ if not utm_venues:
26
+ print("No UTM venues found to update!")
27
+ return
28
+
29
+ updated_count = 0
30
+
31
+ # Update each venue with slight offset from base coordinates
32
+ for idx, venue in enumerate(utm_venues):
33
+ # Create slight variations for each venue (small offsets)
34
+ # This simulates different rooms being in slightly different locations
35
+ lat_offset = (idx % 10) * 0.0001 # Small latitude variation
36
+ lon_offset = (idx // 10) * 0.0001 # Small longitude variation
37
+
38
+ venue.latitude = BASE_LAT + lat_offset
39
+ venue.longitude = BASE_LON + lon_offset
40
+ venue.radius_meters = 50 # Keep 50m radius for classrooms
41
+
42
+ updated_count += 1
43
+ print(f"Updated: {venue.name} -> ({venue.latitude:.6f}, {venue.longitude:.6f})")
44
+
45
+ # Commit all changes
46
+ db.session.commit()
47
+
48
+ print("=" * 70)
49
+ print(f"[OK] Successfully updated {updated_count} UTM venues")
50
+ print(f"Base coordinates: {BASE_LAT}N, {BASE_LON}E")
51
+ print(f"PUTERI-COURT was NOT updated (kept separate)")
52
+ print("=" * 70)
53
+
54
+ # Verify PUTERI-COURT wasn't changed
55
+ puteri = Classroom.query.filter_by(name='PUTERI-COURT').first()
56
+ if puteri:
57
+ print(f"\n[OK] PUTERI-COURT location verified:")
58
+ print(f" Latitude: {puteri.latitude}")
59
+ print(f" Longitude: {puteri.longitude}")
60
+ print(f" (Jalan Raja Chulan, KL - unchanged)")
61
+
62
+ print(f"\nTotal venues in database: {Classroom.query.count()}")
63
+
64
+ if __name__ == '__main__':
65
+ update_utm_coordinates()
utils.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import numpy as np
3
+ from PIL import Image
4
+ import io
5
+ from math import radians, cos, sin, asin, sqrt
6
+
7
+ def base64_to_image(base64_string):
8
+ """Convert base64 string to PIL Image"""
9
+ try:
10
+ # Remove data URL prefix if present
11
+ if ',' in base64_string:
12
+ base64_string = base64_string.split(',')[1]
13
+
14
+ # Decode base64
15
+ image_data = base64.b64decode(base64_string)
16
+ image = Image.open(io.BytesIO(image_data))
17
+
18
+ # Convert to RGB if necessary
19
+ if image.mode != 'RGB':
20
+ image = image.convert('RGB')
21
+
22
+ return image
23
+ except Exception as e:
24
+ raise ValueError(f"Failed to decode base64 image: {str(e)}")
25
+
26
+
27
+ def image_to_numpy(image):
28
+ """Convert PIL Image to numpy array"""
29
+ return np.array(image)
30
+
31
+
32
+ def haversine_distance(lat1, lon1, lat2, lon2):
33
+ """
34
+ Calculate the great circle distance between two points
35
+ on the earth (specified in decimal degrees)
36
+ Returns distance in meters
37
+ """
38
+ # Convert decimal degrees to radians
39
+ lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2])
40
+
41
+ # Haversine formula
42
+ dlat = lat2 - lat1
43
+ dlon = lon2 - lon1
44
+ a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
45
+ c = 2 * asin(sqrt(a))
46
+
47
+ # Radius of earth in meters
48
+ r = 6371000
49
+
50
+ return c * r
51
+
52
+
53
+ def format_error_response(message, code=400):
54
+ """Format error response"""
55
+ return {
56
+ 'success': False,
57
+ 'error': message
58
+ }, code
59
+
60
+
61
+ def format_success_response(data=None, message=None):
62
+ """Format success response"""
63
+ response = {'success': True}
64
+ if message:
65
+ response['message'] = message
66
+ if data:
67
+ response['data'] = data
68
+ return response, 200
69
+
70
+
71
+ def validate_coordinates(latitude, longitude):
72
+ """Validate GPS coordinates"""
73
+ try:
74
+ lat = float(latitude)
75
+ lon = float(longitude)
76
+
77
+ if not (-90 <= lat <= 90):
78
+ return False, "Latitude must be between -90 and 90"
79
+ if not (-180 <= lon <= 180):
80
+ return False, "Longitude must be between -180 and 180"
81
+
82
+ return True, None
83
+ except (ValueError, TypeError):
84
+ return False, "Invalid coordinate format"