Jun 29 - Implement auto reminder 45 min before user's shift end if not finish checking websites

This commit is contained in:
2026-06-29 17:03:07 -04:00
parent 176e19213c
commit 56a256633b
4 changed files with 59 additions and 7 deletions
+44 -2
View File
@@ -17,6 +17,46 @@ logging.basicConfig(
logger = logging.getLogger("app")
def _run_reminder_job():
"""Background job: send shift-incomplete reminders if within the reminder window."""
from routes.admin_shifts import do_send_incomplete_reminders
try:
result = do_send_incomplete_reminders(triggered_by_user_id=None)
if result["sent"] > 0:
logger.info(f"[scheduler] Shift reminders sent: {result}")
except Exception as e:
logger.error(f"[scheduler] Shift reminder job error: {e}")
def _start_scheduler():
"""
Start a background scheduler that fires shift-incomplete reminder emails
every 5 minutes. Guards against double-start in Flask's debug reloader.
Uses daemon threads so the process exits cleanly even if the scheduler is running.
"""
if os.environ.get("WERKZEUG_RUN_MAIN") == "false":
# Flask reloader: skip in the parent monitor process; the child sets WERKZEUG_RUN_MAIN=true
return
try:
from apscheduler.schedulers.background import BackgroundScheduler
import atexit
scheduler = BackgroundScheduler(daemon=True)
scheduler.add_job(
func=_run_reminder_job,
trigger="interval",
minutes=5,
id="shift_incomplete_reminders",
max_instances=1,
misfire_grace_time=120,
)
scheduler.start()
atexit.register(lambda: scheduler.shutdown(wait=False))
logger.info("[scheduler] Background scheduler started — shift reminders every 5 min.")
except Exception as e:
logger.error(f"[scheduler] Failed to start background scheduler: {e}")
def create_app():
app = Flask(__name__)
@@ -73,10 +113,12 @@ def create_app():
if not os.environ.get("CRON_SECRET"):
logger.warning(
"CRON_SECRET not set in .env — the /internal/cron/* endpoints are "
"disabled. Set CRON_SECRET to enable automated shift reminders."
"CRON_SECRET not set in .env — the /internal/cron/* endpoints are disabled."
)
# ── Background scheduler: automatic shift-incomplete reminders ────────────
_start_scheduler()
# ── CSRF protection (Flask-WTF) ────────────────────────────────────────────
from flask_wtf.csrf import CSRFProtect
CSRFProtect(app)
+7 -2
View File
@@ -1771,8 +1771,11 @@ def get_incomplete_shift_users_near_end(window_minutes: int = 40) -> list:
conn.close()
def record_shift_reminder(user_id: int, shift_id: int) -> None:
"""Mark that a reminder was sent to this user for this shift today (idempotent)."""
def record_shift_reminder(user_id: int, shift_id: int) -> int:
"""
Atomically claim a reminder slot for this user+shift today via INSERT IGNORE.
Returns 1 if the slot was claimed (caller should send), 0 if already claimed.
"""
conn = None
try:
conn = get_connection()
@@ -1783,7 +1786,9 @@ def record_shift_reminder(user_id: int, shift_id: int) -> None:
(user_id, shift_id),
)
conn.commit()
rows = cur.rowcount
cur.close()
return rows
finally:
if conn:
conn.close()
+1
View File
@@ -11,3 +11,4 @@ bcrypt==4.1.3
pypdf==4.3.1
python-docx==1.1.2
openpyxl==3.1.5
APScheduler>=3.10,<4
+7 -3
View File
@@ -118,7 +118,7 @@ def edit(shift_id):
return redirect(url_for("admin_shifts.shifts_list"))
REMINDER_WINDOW_MIN = 40
REMINDER_WINDOW_MIN = 45
def _fmt_time(t) -> str:
@@ -144,6 +144,11 @@ def do_send_incomplete_reminders(triggered_by_user_id=None) -> dict:
skipped += 1
continue
# Atomically claim this reminder slot before sending.
# If another worker already claimed it (rowcount == 0), skip to avoid duplicates.
if not record_shift_reminder(row["user_id"], row["shift_id"]):
continue
end_str = _fmt_time(row["end_time"])
site_lines = "\n".join(
f"{s['name']}{s['url']}" for s in row["unchecked_sites"]
@@ -163,7 +168,6 @@ def do_send_incomplete_reminders(triggered_by_user_id=None) -> dict:
f"Action required — complete your shift checks by {end_str}",
body,
)
record_shift_reminder(row["user_id"], row["shift_id"])
log_action(
triggered_by_user_id, "SHIFT_INCOMPLETE_REMINDER", "shifts", row["shift_id"],
f"Reminder sent to {row['username']} ({row['email']}): "
@@ -180,7 +184,7 @@ def do_send_incomplete_reminders(triggered_by_user_id=None) -> dict:
@admin_shifts_bp.route("/send-incomplete-reminders", methods=["POST"])
@admin_required
def send_incomplete_reminders():
"""Manual trigger: email staff with incomplete checks in shifts ending within 40 min."""
"""Manual trigger: email staff with incomplete checks in shifts ending within 45 min."""
result = do_send_incomplete_reminders(triggered_by_user_id=session["user"]["id"])
if result["total"] == 0: