from flask import Flask, request, jsonify, render_template_string, redirect from flask_sqlalchemy import SQLAlchemy from datetime import datetime, timezone, timedelta app = Flask(__name__) app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///rfid.db" app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False db = SQLAlchemy(app) # -------------------------------------------------- # MODELS # -------------------------------------------------- class Student(db.Model): id = db.Column(db.Integer, primary_key=True) uid = db.Column(db.String(64), unique=True) name = db.Column(db.String(128)) current_room = db.Column(db.String(64)) previous_room = db.Column(db.String(64)) expected_return = db.Column(db.String(64)) state = db.Column(db.String(32), default="IN_ROOM") last_scan = db.Column(db.DateTime) last_reader = db.Column(db.String(64)) class Event(db.Model): id = db.Column(db.Integer, primary_key=True) uid = db.Column(db.String(64)) room = db.Column(db.String(64)) event_type = db.Column(db.String(64)) timestamp = db.Column(db.DateTime) class Bathroom(db.Model): id = db.Column(db.Integer, primary_key=True) room = db.Column(db.String(64), unique=True) count = db.Column(db.Integer, default=0) max = db.Column(db.Integer, default=2) class Anomaly(db.Model): id = db.Column(db.Integer, primary_key=True) uid = db.Column(db.String(64)) type = db.Column(db.String(64)) timestamp = db.Column(db.DateTime) # -------------------------------------------------- # UTIL # -------------------------------------------------- COOLDOWN = 3 def now(): return datetime.now(timezone.utc) def recent_scan(student): if not student.last_scan: return False return (now() - student.last_scan).total_seconds() < COOLDOWN # -------------------------------------------------- # MOVEMENT ENGINE # -------------------------------------------------- def process_scan(student, room): timestamp = now() # duplicate suppression if recent_scan(student) and student.last_reader == room: return None student.last_scan = timestamp student.last_reader = room event_type = "move" # bathroom logic if room.startswith("bathroom"): if student.state != "IN_BATHROOM": student.previous_room = student.current_room student.current_room = room student.state = "IN_BATHROOM" student.expected_return = student.previous_room event_type = "bathroom_enter" else: student.current_room = "hallway" student.state = "IN_HALLWAY" event_type = "bathroom_exit" else: # returning from bathroom if student.state == "IN_HALLWAY" and student.expected_return == room: student.expected_return = None student.previous_room = student.current_room student.current_room = room student.state = "IN_ROOM" db.session.add(Event( uid=student.uid, room=room, event_type=event_type, timestamp=timestamp )) # anomaly detection if student.expected_return and room != student.expected_return and student.state == "IN_HALLWAY": db.session.add(Anomaly( uid=student.uid, type="WRONG_RETURN", timestamp=timestamp )) db.session.commit() # -------------------------------------------------- # ROUTES # -------------------------------------------------- @app.route("/scan", methods=["POST"]) def scan(): data = request.json student = Student.query.filter_by(uid=data["uid"]).first() if not student: student = Student(uid=data["uid"], name="Unknown") db.session.add(student) db.session.commit() process_scan(student, data["room"]) return jsonify({"status": "ok"}) # -------------------------------------------------- # ADMIN DASHBOARD # -------------------------------------------------- ADMIN_HTML = """