Fix issues #8, #9, #10, #11: scheduling conflicts, med reminders, adaptive timing, nagging

- #11: Add validation to prevent simultaneous scheduling of routines and medications
  - Added _check_schedule_conflicts() in routines.py
  - Added _check_med_schedule_conflicts() in medications.py
  - Returns HTTP 409 with descriptive error on conflict

- #10: Fix medication reminders not being sent
  - Added call to check_adaptive_medication_reminders() in daemon poll loop

- #9: Fix can't enable adaptive timing
  - Added proper error handling and logging in update_adaptive_settings()
  - Returns meaningful error message on database failures

- #8: Fix nagging not working
  - Added debug logging for missing settings
  - Auto-create medication schedules if they don't exist
  - Improved error logging (warning -> error)
This commit is contained in:
Chelsea
2026-02-17 04:20:34 +00:00
parent 3aad7a4867
commit a2c7940a5c
4 changed files with 166 additions and 25 deletions

View File

@@ -2,12 +2,15 @@
api/routes/adaptive_meds.py - API endpoints for adaptive medication settings api/routes/adaptive_meds.py - API endpoints for adaptive medication settings
""" """
import logging
import flask import flask
import jwt import jwt
import os import os
import core.postgres as postgres import core.postgres as postgres
import core.adaptive_meds as adaptive_meds import core.adaptive_meds as adaptive_meds
logger = logging.getLogger(__name__)
JWT_SECRET = os.getenv("JWT_SECRET") JWT_SECRET = os.getenv("JWT_SECRET")
@@ -99,18 +102,25 @@ def register(app):
"quiet_hours_end": data.get("quiet_hours_end"), "quiet_hours_end": data.get("quiet_hours_end"),
} }
# Check if settings exist try:
existing = adaptive_meds.get_adaptive_settings(user_uuid) # Check if settings exist
existing = adaptive_meds.get_adaptive_settings(user_uuid)
if existing: if existing:
postgres.update( postgres.update(
"adaptive_med_settings", update_data, {"user_uuid": user_uuid} "adaptive_med_settings", update_data, {"user_uuid": user_uuid}
) )
else: else:
update_data["user_uuid"] = user_uuid update_data["user_uuid"] = user_uuid
postgres.insert("adaptive_med_settings", update_data) postgres.insert("adaptive_med_settings", update_data)
return flask.jsonify({"success": True}), 200 return flask.jsonify({"success": True}), 200
except Exception as e:
logger.error(f"Failed to save adaptive settings: {e}")
return flask.jsonify({
"error": "Failed to save settings",
"details": str(e)
}), 500
@app.route("/api/adaptive-meds/presence", methods=["GET"]) @app.route("/api/adaptive-meds/presence", methods=["GET"])
def get_presence_status(): def get_presence_status():

View File

@@ -2,6 +2,7 @@
Medications API - medication scheduling, logging, and adherence tracking Medications API - medication scheduling, logging, and adherence tracking
""" """
import json
import os import os
import uuid import uuid
from datetime import datetime, date, timedelta, timezone from datetime import datetime, date, timedelta, timezone
@@ -144,6 +145,35 @@ def register(app):
meds = postgres.select("medications", where={"user_uuid": user_uuid}, order_by="name") meds = postgres.select("medications", where={"user_uuid": user_uuid}, order_by="name")
return flask.jsonify(meds), 200 return flask.jsonify(meds), 200
def _check_med_schedule_conflicts(user_uuid, new_times, new_days=None, exclude_med_id=None):
"""Check if the proposed medication schedule conflicts with existing routines or medications.
Returns (has_conflict, conflict_message) tuple.
"""
if not new_times:
return False, None
# Check conflicts with routines
user_routines = postgres.select("routines", {"user_uuid": user_uuid})
for r in user_routines:
sched = postgres.select_one("routine_schedules", {"routine_id": r["id"]})
if sched and sched.get("time") in new_times:
routine_days = json.loads(sched.get("days", "[]"))
if not new_days or any(d in routine_days for d in new_days):
return True, f"Time conflicts with routine: {r.get('name', 'Unnamed routine')}"
# Check conflicts with other medications
user_meds = postgres.select("medications", {"user_uuid": user_uuid, "active": True})
for med in user_meds:
if med["id"] == exclude_med_id:
continue
med_times = med.get("times", [])
if isinstance(med_times, str):
med_times = json.loads(med_times)
if any(t in med_times for t in new_times):
return True, f"Time conflicts with medication: {med.get('name', 'Unnamed medication')}"
return False, None
@app.route("/api/medications", methods=["POST"]) @app.route("/api/medications", methods=["POST"])
def api_addMedication(): def api_addMedication():
"""Add a medication. Body: {name, dosage, unit, frequency, times?, days_of_week?, interval_days?, start_date?, notes?}""" """Add a medication. Body: {name, dosage, unit, frequency, times?, days_of_week?, interval_days?, start_date?, notes?}"""
@@ -158,6 +188,15 @@ def register(app):
if missing: if missing:
return flask.jsonify({"error": f"missing required fields: {', '.join(missing)}"}), 400 return flask.jsonify({"error": f"missing required fields: {', '.join(missing)}"}), 400
# Check for schedule conflicts
new_times = data.get("times", [])
new_days = data.get("days_of_week", [])
has_conflict, conflict_msg = _check_med_schedule_conflicts(
user_uuid, new_times, new_days
)
if has_conflict:
return flask.jsonify({"error": conflict_msg}), 409
row = { row = {
"id": str(uuid.uuid4()), "id": str(uuid.uuid4()),
"user_uuid": user_uuid, "user_uuid": user_uuid,
@@ -217,6 +256,17 @@ def register(app):
"name", "dosage", "unit", "frequency", "times", "notes", "active", "name", "dosage", "unit", "frequency", "times", "notes", "active",
"days_of_week", "interval_days", "start_date", "next_dose_date", "days_of_week", "interval_days", "start_date", "next_dose_date",
] ]
# Check for schedule conflicts if times are being updated
if "times" in data:
new_times = data.get("times", [])
new_days = data.get("days_of_week") or existing.get("days_of_week", [])
has_conflict, conflict_msg = _check_med_schedule_conflicts(
user_uuid, new_times, new_days, exclude_med_id=med_id
)
if has_conflict:
return flask.jsonify({"error": conflict_msg}), 409
updates = {k: v for k, v in data.items() if k in allowed} updates = {k: v for k, v in data.items() if k in allowed}
if not updates: if not updates:
return flask.jsonify({"error": "no valid fields to update"}), 400 return flask.jsonify({"error": "no valid fields to update"}), 400

View File

@@ -649,6 +649,40 @@ def register(app):
) )
return flask.jsonify(result), 200 return flask.jsonify(result), 200
def _check_schedule_conflicts(user_uuid, new_days, new_time, exclude_routine_id=None):
"""Check if the proposed schedule conflicts with existing routines or medications.
Returns (has_conflict, conflict_message) tuple.
"""
if not new_days or not new_time:
return False, None
# Check conflicts with other routines
user_routines = postgres.select("routines", {"user_uuid": user_uuid})
for r in user_routines:
if r["id"] == exclude_routine_id:
continue
other_sched = postgres.select_one("routine_schedules", {"routine_id": r["id"]})
if other_sched and other_sched.get("time") == new_time:
other_days = json.loads(other_sched.get("days", "[]"))
if any(d in other_days for d in new_days):
return True, f"Time conflicts with routine: {r.get('name', 'Unnamed routine')}"
# Check conflicts with medications
user_meds = postgres.select("medications", {"user_uuid": user_uuid, "active": True})
for med in user_meds:
med_times = med.get("times", [])
if isinstance(med_times, str):
med_times = json.loads(med_times)
if new_time in med_times:
# Check if medication runs on any of the same days
med_days = med.get("days_of_week", [])
if isinstance(med_days, str):
med_days = json.loads(med_days)
if not med_days or any(d in med_days for d in new_days):
return True, f"Time conflicts with medication: {med.get('name', 'Unnamed medication')}"
return False, None
@app.route("/api/routines/<routine_id>/schedule", methods=["PUT"]) @app.route("/api/routines/<routine_id>/schedule", methods=["PUT"])
def api_setRoutineSchedule(routine_id): def api_setRoutineSchedule(routine_id):
"""Set when this routine should run. Body: {days: ["mon","tue",...], time: "08:00", remind: true}""" """Set when this routine should run. Body: {days: ["mon","tue",...], time: "08:00", remind: true}"""
@@ -663,6 +697,16 @@ def register(app):
data = flask.request.get_json() data = flask.request.get_json()
if not data: if not data:
return flask.jsonify({"error": "missing body"}), 400 return flask.jsonify({"error": "missing body"}), 400
# Check for schedule conflicts
new_days = data.get("days", [])
new_time = data.get("time")
has_conflict, conflict_msg = _check_schedule_conflicts(
user_uuid, new_days, new_time, exclude_routine_id=routine_id
)
if has_conflict:
return flask.jsonify({"error": conflict_msg}), 409
existing = postgres.select_one("routine_schedules", {"routine_id": routine_id}) existing = postgres.select_one("routine_schedules", {"routine_id": routine_id})
schedule_data = { schedule_data = {
"routine_id": routine_id, "routine_id": routine_id,

View File

@@ -305,22 +305,56 @@ def check_nagging():
# Get user's settings # Get user's settings
settings = adaptive_meds.get_adaptive_settings(user_uuid) settings = adaptive_meds.get_adaptive_settings(user_uuid)
if not settings or not settings.get("nagging_enabled"): if not settings:
logger.debug(f"No adaptive settings for user {user_uuid}")
continue
if not settings.get("nagging_enabled"):
logger.debug(f"Nagging disabled for user {user_uuid}")
continue continue
now = datetime.utcnow() now = datetime.utcnow()
today = now.date() today = now.date()
# Get today's schedules # Get today's schedules
schedules = postgres.select( try:
"medication_schedules", schedules = postgres.select(
where={ "medication_schedules",
"user_uuid": user_uuid, where={
"medication_id": med_id, "user_uuid": user_uuid,
"adjustment_date": today, "medication_id": med_id,
"status": "pending", "adjustment_date": today,
}, "status": "pending",
) },
)
except Exception as e:
logger.warning(f"Could not query medication_schedules for {med_id}: {e}")
# Table may not exist yet
continue
# If no schedules exist, try to create them
if not schedules:
logger.info(f"No schedules found for medication {med_id}, attempting to create")
times = med.get("times", [])
if times:
try:
adaptive_meds.create_daily_schedule(user_uuid, med_id, times)
# Re-query for schedules
schedules = postgres.select(
"medication_schedules",
where={
"user_uuid": user_uuid,
"medication_id": med_id,
"adjustment_date": today,
"status": "pending",
},
)
except Exception as e:
logger.warning(f"Could not create schedules for {med_id}: {e}")
continue
if not schedules:
logger.debug(f"No pending schedules for medication {med_id}")
continue
for sched in schedules: for sched in schedules:
# Check if we should nag # Check if we should nag
@@ -406,16 +440,19 @@ def poll_callback():
f"Could not create adaptive schedules (tables may not exist): {e}" f"Could not create adaptive schedules (tables may not exist): {e}"
) )
# Check reminders - ALWAYS use original check for now # Check reminders - use both original and adaptive checks
# (adaptive check requires database migration) logger.info("Checking medication reminders")
logger.info("Checking medication reminders (using original method)")
check_medication_reminders() check_medication_reminders()
try:
check_adaptive_medication_reminders()
except Exception as e:
logger.warning(f"Adaptive medication reminder check failed: {e}")
# Check for nags # Check for nags - log as error to help with debugging
try: try:
check_nagging() check_nagging()
except Exception as e: except Exception as e:
logger.warning(f"Nagging check failed: {e}") logger.error(f"Nagging check failed: {e}")
# Original checks # Original checks
check_routine_reminders() check_routine_reminders()