- Add all 14 missing database tables (medications, med_logs, routines, etc.) - Rewrite medication scheduling: support specific days, every N days, as-needed (PRN) - Fix taken_times matching: match by created_at date, not scheduled_time string - Fix adherence calculation: taken / expected doses, not taken / (taken + skipped) - Add formatSchedule() helper for readable display - Update client types and API layer - Rename brilli-ins-client → synculous-client - Make client PWA: add manifest, service worker, icons - Bind dev server to 0.0.0.0 for network access - Fix SVG icon bugs in Icons.tsx - Add .dockerignore for client npm caching Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
140 lines
4.7 KiB
Python
140 lines
4.7 KiB
Python
"""
|
|
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
|
|
|
|
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 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})
|
|
now = datetime.now()
|
|
current_time = now.strftime("%H:%M")
|
|
current_day = now.strftime("%a").lower() # "mon","tue", etc.
|
|
today = now.date()
|
|
today_str = today.isoformat()
|
|
|
|
for med in meds:
|
|
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":
|
|
days = med.get("days_of_week", [])
|
|
if current_day not in 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(med["user_uuid"])
|
|
if user_settings:
|
|
msg = f"Time to take {med['name']} ({med['dosage']} {med['unit']})"
|
|
notifications._sendToEnabledChannels(user_settings, msg)
|
|
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:
|
|
now = datetime.now()
|
|
current_time = now.strftime("%H:%M")
|
|
current_day = now.strftime("%a").lower()
|
|
schedules = postgres.select("routine_schedules", where={"remind": True})
|
|
|
|
for schedule in schedules:
|
|
if current_time != schedule.get("time"):
|
|
continue
|
|
days = schedule.get("days", [])
|
|
if current_day not in days:
|
|
continue
|
|
|
|
routine = postgres.select_one("routines", {"id": schedule["routine_id"]})
|
|
if not routine:
|
|
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)
|
|
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)
|
|
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()
|