""" daemon.py - Background polling loop for scheduled tasks Override poll_callback() with your domain-specific logic. """ import os import time import logging from datetime import datetime, timezone, timedelta import core.postgres as postgres import core.notifications as notifications logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL", 60)) def _user_now_for(user_uuid): """Get current datetime in a user's timezone using their stored offset.""" prefs = postgres.select_one("user_preferences", {"user_uuid": user_uuid}) offset_minutes = 0 if prefs and prefs.get("timezone_offset") is not None: offset_minutes = prefs["timezone_offset"] # JS getTimezoneOffset: positive = behind UTC, so negate tz_obj = timezone(timedelta(minutes=-offset_minutes)) return datetime.now(tz_obj) def check_medication_reminders(): """Check for medications due now and send notifications.""" try: from datetime import date as date_type meds = postgres.select("medications", where={"active": True}) # Group by user so we only look up timezone once per user user_meds = {} for med in meds: uid = med.get("user_uuid") if uid not in user_meds: user_meds[uid] = [] user_meds[uid].append(med) for user_uuid, user_med_list in user_meds.items(): now = _user_now_for(user_uuid) current_time = now.strftime("%H:%M") current_day = now.strftime("%a").lower() today = now.date() today_str = today.isoformat() for med in user_med_list: freq = med.get("frequency", "daily") # Skip as_needed -- no scheduled reminders for PRN if freq == "as_needed": continue # Day-of-week check for specific_days if freq == "specific_days": med_days = med.get("days_of_week", []) if current_day not in med_days: continue # Interval check for every_n_days if freq == "every_n_days": start = med.get("start_date") interval = med.get("interval_days") if start and interval: start_d = ( start if isinstance(start, date_type) else datetime.strptime(str(start), "%Y-%m-%d").date() ) if (today - start_d).days < 0 or ( today - start_d ).days % interval != 0: continue else: continue # Time check times = med.get("times", []) if current_time not in times: continue # Already taken today? Check by created_at date logs = postgres.select( "med_logs", where={"medication_id": med["id"], "action": "taken"} ) already_taken = any( log.get("scheduled_time") == current_time and str(log.get("created_at", ""))[:10] == today_str for log in logs ) if already_taken: continue user_settings = notifications.getNotificationSettings(user_uuid) if user_settings: msg = f"Time to take {med['name']} ({med['dosage']} {med['unit']}) ยท {current_time}" notifications._sendToEnabledChannels( user_settings, msg, user_uuid=user_uuid ) except Exception as e: logger.error(f"Error checking medication reminders: {e}") def check_routine_reminders(): """Check for scheduled routines due now and send notifications.""" try: schedules = postgres.select("routine_schedules", where={"remind": True}) for schedule in schedules: routine = postgres.select_one("routines", {"id": schedule["routine_id"]}) if not routine: continue now = _user_now_for(routine["user_uuid"]) current_time = now.strftime("%H:%M") current_day = now.strftime("%a").lower() if current_time != schedule.get("time"): continue days = schedule.get("days", []) if current_day not in days: continue user_settings = notifications.getNotificationSettings(routine["user_uuid"]) if user_settings: msg = f"Time to start your routine: {routine['name']}" notifications._sendToEnabledChannels( user_settings, msg, user_uuid=routine["user_uuid"] ) except Exception as e: logger.error(f"Error checking routine reminders: {e}") def check_refills(): """Check for medications running low on refills.""" try: meds = postgres.select("medications") for med in meds: qty = med.get("quantity_remaining") if qty is not None and qty <= 7: user_settings = notifications.getNotificationSettings(med["user_uuid"]) if user_settings: msg = f"Low on {med['name']}: only {qty} doses remaining. Time to refill!" notifications._sendToEnabledChannels( user_settings, msg, user_uuid=med["user_uuid"] ) except Exception as e: logger.error(f"Error checking refills: {e}") def poll_callback(): """Called every POLL_INTERVAL seconds.""" check_medication_reminders() check_routine_reminders() check_refills() def daemon_loop(): logger.info("Scheduler daemon starting") while True: try: poll_callback() except Exception as e: logger.error(f"Poll callback error: {e}") time.sleep(POLL_INTERVAL) if __name__ == "__main__": daemon_loop()