QR Attendance Flow
This is the system's central feature: a teacher scans a student's QR code and the student is marked present for the selected subject. The logic lives in scanQrAttendance() in src/app/actions/attendance.ts, invoked from src/components/QrScanner.tsx.
End-to-end sequence
Validation steps (in order)
scanQrAttendance() short-circuits with a specific message at each gate:
- Authorization — throws unless the caller's role is
TEACHER. - Student lookup — the
qrTokenmust map to a real student, else "Invalid QR code — student not found." - Subject & ownership — the subject must exist and its
teacherIdmust equal the scanning teacher's id, else "You are not the teacher for …". - Enrollment — the student must have a
StudentSubjectrow for that subject, else "… is not enrolled in …". - Duplicate guard — if the student is already
PRESENTorLATEtoday for that subject, the scan is skipped: "… is already marked PRESENT/LATE today." - Lateness decision —
decideScanStatus()(src/lib/schedule.ts) compares the scan's Manila time to the subject's scheduled start time plus thelate_grace_minutessystem setting, yieldingPRESENTorLATE. - Commit — inside a transaction, upsert the attendance to that status and create an
AttendanceAuditrow withreason: "QR Scan".
What gets written
Attendance— upserted on the unique key(studentId, date, subjectId)wheredateis today's UTC midnight. Status becomesPRESENTorLATE.AttendanceAudit— a new immutable row recordingoldStatus,newStatus, who changed it, andreason = "QR Scan".ActivityLog— anATTENDANCE_SCANentry.
Attendance statuses
| Status | How it's set |
|---|---|
PRESENT | By a QR scan within the late_grace_minutes window, a manual toggle, or an approved appeal. |
LATE | Automatically by a QR scan made after late_grace_minutes past the subject's scheduled start time, or by a manual toggle. |
ABSENT | Automatically by the auto-absent cron sweep once class has ended with no scan, or by a manual toggle. |
Auto LATE and auto ABSENT
Both "late" and "absent" are decided by the system, not left for a teacher to toggle by hand:
- LATE is decided at scan time, as described above. E.g. a class starting 8:00 AM with
late_grace_minutesset to 30 marks any scan after 8:30 AM asLATEinstead ofPRESENT. Configure the grace period from Admin Dashboard → System Settings. - ABSENT is decided by a scheduled sweep, since "no scan happened" can't be detected by a request handler.
runAutoAbsentSweep()(src/lib/autoAbsent.ts) is invoked by the/api/cron/mark-absentroute (protected byCRON_SECRETthe same way as the backup cron). On each run it finds subjects whose session has ended for today — using the schedule's explicit end time (e.g. the09:30in08:00-09:30), orstart + late_grace_minuteswhen no end time is set — and creates anABSENTAttendancerow (with anAttendanceAuditrow,changedBy: "SYSTEM") for every enrolled student who still has no record for that subject/date. It never touches a row that already exists, so it's safe to run repeatedly and it never overwrites a scan, a manual edit, or an appeal. Turn the whole sweep off with theauto_absent_enabledsystem setting. The Vercel Hobby plan only allows daily cron jobs, so the five-minute sweep is intentionally not registered invercel.json; a Raspberry Pi systemd timer calls it instead. See Raspberry Pi attendance scheduler. - Appeals still work exactly the same way against auto-marked records: a student can appeal an auto-
ABSENTor auto-LATEday like any other, and an approved appeal upserts theAttendancerow toPRESENT. See Appeals.
Manual alternative
When scanning isn't possible, updateAttendance() handles manual roster toggles. It applies the time-lock (teachers can't edit records older than attendance_lock_hours; admins can) and writes the same audit trail. See the Teacher Guide.