- #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:
@@ -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,6 +102,7 @@ def register(app):
|
|||||||
"quiet_hours_end": data.get("quiet_hours_end"),
|
"quiet_hours_end": data.get("quiet_hours_end"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
# Check if settings exist
|
# Check if settings exist
|
||||||
existing = adaptive_meds.get_adaptive_settings(user_uuid)
|
existing = adaptive_meds.get_adaptive_settings(user_uuid)
|
||||||
|
|
||||||
@@ -111,6 +115,12 @@ def register(app):
|
|||||||
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():
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -305,13 +305,18 @@ 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
|
||||||
|
try:
|
||||||
schedules = postgres.select(
|
schedules = postgres.select(
|
||||||
"medication_schedules",
|
"medication_schedules",
|
||||||
where={
|
where={
|
||||||
@@ -321,6 +326,35 @@ def check_nagging():
|
|||||||
"status": "pending",
|
"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()
|
||||||
|
|||||||
Reference in New Issue
Block a user