Fix adaptive medication timing and update README
- Fix double notifications: remove redundant check_medication_reminders() call, use adaptive path as primary with basic as fallback - Fix nag firing immediately: require nag_interval minutes after scheduled dose time before first nag - Fix missing schedules: create on-demand if midnight window was missed - Fix wrong timezone: use user_now_for() instead of request-context user_now() in calculate_adjusted_times() - Fix immutable schedules: recalculate pending schedules on wake event detection so adaptive timing actually adapts - Fix take/skip not updating schedule: API endpoints now call mark_med_taken/skipped so nags stop after logging a dose - Fix skipped doses still triggering reminders: check both taken and skipped in adaptive reminder and log queries - Update README with tasks, AI step generation, auth refresh tokens, knowledge base improvements, and current architecture Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ import uuid
|
||||
from datetime import datetime, timedelta, time, timezone
|
||||
from typing import Optional, Dict, List, Tuple
|
||||
import core.postgres as postgres
|
||||
from core.tz import user_now, user_today_for, tz_for_user
|
||||
from core.tz import user_now, user_now_for, user_today_for, tz_for_user
|
||||
|
||||
|
||||
def _normalize_time(val):
|
||||
@@ -42,12 +42,24 @@ def get_user_presence(user_uuid: str) -> Optional[Dict]:
|
||||
|
||||
|
||||
def update_user_presence(user_uuid: str, discord_user_id: str, is_online: bool):
|
||||
"""Update user's presence status."""
|
||||
"""Update user's presence status. If a wake event is detected (came online
|
||||
after 30+ minutes offline), recalculates today's adaptive medication schedules."""
|
||||
now = datetime.utcnow()
|
||||
|
||||
presence = get_user_presence(user_uuid)
|
||||
is_wake_event = False
|
||||
|
||||
if presence:
|
||||
# Detect wake event before updating
|
||||
if is_online and not presence.get("is_currently_online"):
|
||||
last_offline = presence.get("last_offline_at")
|
||||
if last_offline:
|
||||
if isinstance(last_offline, datetime) and last_offline.tzinfo is None:
|
||||
last_offline = last_offline.replace(tzinfo=timezone.utc)
|
||||
offline_duration = (now.replace(tzinfo=timezone.utc) - last_offline).total_seconds()
|
||||
if offline_duration > 1800: # 30 minutes
|
||||
is_wake_event = True
|
||||
|
||||
# Update existing record
|
||||
updates = {"is_currently_online": is_online, "updated_at": now}
|
||||
|
||||
@@ -71,6 +83,26 @@ def update_user_presence(user_uuid: str, discord_user_id: str, is_online: bool):
|
||||
}
|
||||
postgres.insert("user_presence", data)
|
||||
|
||||
# On wake event, recalculate today's adaptive schedules
|
||||
if is_wake_event:
|
||||
_recalculate_schedules_on_wake(user_uuid, now)
|
||||
|
||||
|
||||
def _recalculate_schedules_on_wake(user_uuid: str, wake_time: datetime):
|
||||
"""Recalculate today's pending adaptive schedules using the actual wake time."""
|
||||
settings = get_adaptive_settings(user_uuid)
|
||||
if not settings or not settings.get("adaptive_timing_enabled"):
|
||||
return
|
||||
|
||||
try:
|
||||
meds = postgres.select("medications", {"user_uuid": user_uuid, "active": True})
|
||||
for med in meds:
|
||||
times = med.get("times", [])
|
||||
if times:
|
||||
create_daily_schedule(user_uuid, med["id"], times, recalculate=True)
|
||||
except Exception:
|
||||
pass # Best-effort — don't break presence tracking if this fails
|
||||
|
||||
|
||||
def record_presence_event(user_uuid: str, event_type: str, timestamp: datetime):
|
||||
"""Record a presence event in the history."""
|
||||
@@ -182,12 +214,8 @@ def calculate_adjusted_times(
|
||||
# Return base times with 0 offset
|
||||
return [(t, 0) for t in base_times]
|
||||
|
||||
# Get user's timezone
|
||||
prefs = postgres.select("user_preferences", {"user_uuid": user_uuid})
|
||||
offset_minutes = prefs[0].get("timezone_offset", 0) if prefs else 0
|
||||
|
||||
# Get current time in user's timezone
|
||||
user_current_time = user_now(offset_minutes)
|
||||
# Get current time in user's timezone (works in both request and scheduler context)
|
||||
user_current_time = user_now_for(user_uuid)
|
||||
today = user_current_time.date()
|
||||
|
||||
# Determine wake time
|
||||
@@ -296,6 +324,14 @@ def should_send_nag(
|
||||
time_since_last_nag = (current_time - last_nag).total_seconds() / 60
|
||||
if time_since_last_nag < nag_interval:
|
||||
return False, f"Too soon ({int(time_since_last_nag)} < {nag_interval} min)"
|
||||
else:
|
||||
# First nag: require at least nag_interval minutes since the scheduled dose time
|
||||
if scheduled_time:
|
||||
sched_hour, sched_min = int(scheduled_time[:2]), int(scheduled_time[3:5])
|
||||
sched_dt = current_time.replace(hour=sched_hour, minute=sched_min, second=0, microsecond=0)
|
||||
minutes_since_dose = (current_time - sched_dt).total_seconds() / 60
|
||||
if minutes_since_dose < nag_interval:
|
||||
return False, f"Too soon after dose time ({int(minutes_since_dose)} < {nag_interval} min)"
|
||||
|
||||
# Check if this specific dose was already taken or skipped today
|
||||
logs = postgres.select(
|
||||
@@ -389,8 +425,13 @@ def record_nag_sent(user_uuid: str, med_id: str, scheduled_time):
|
||||
)
|
||||
|
||||
|
||||
def create_daily_schedule(user_uuid: str, med_id: str, base_times: List[str]):
|
||||
"""Create today's medication schedule with adaptive adjustments."""
|
||||
def create_daily_schedule(user_uuid: str, med_id: str, base_times: List[str], recalculate: bool = False):
|
||||
"""Create today's medication schedule with adaptive adjustments.
|
||||
|
||||
If recalculate=True, deletes existing *pending* schedules and recreates them
|
||||
with updated adaptive timing (e.g. after a wake event is detected).
|
||||
Already-taken or skipped schedules are preserved.
|
||||
"""
|
||||
today = user_today_for(user_uuid)
|
||||
|
||||
# Check if schedule already exists
|
||||
@@ -399,9 +440,26 @@ def create_daily_schedule(user_uuid: str, med_id: str, base_times: List[str]):
|
||||
{"user_uuid": user_uuid, "medication_id": med_id, "adjustment_date": today},
|
||||
)
|
||||
|
||||
if existing:
|
||||
if existing and not recalculate:
|
||||
return
|
||||
|
||||
if existing and recalculate:
|
||||
# Only delete pending schedules — preserve taken/skipped
|
||||
for sched in existing:
|
||||
if sched.get("status") == "pending":
|
||||
postgres.delete("medication_schedules", {"id": sched["id"]})
|
||||
# Check if any pending remain to create
|
||||
remaining = [s for s in existing if s.get("status") != "pending"]
|
||||
completed_base_times = set()
|
||||
for s in remaining:
|
||||
bt = _normalize_time(s.get("base_time"))
|
||||
if bt:
|
||||
completed_base_times.add(bt)
|
||||
# Only create schedules for times that haven't been taken/skipped
|
||||
base_times = [t for t in base_times if t not in completed_base_times]
|
||||
if not base_times:
|
||||
return
|
||||
|
||||
# Calculate adjusted times
|
||||
adjusted_times = calculate_adjusted_times(user_uuid, base_times)
|
||||
|
||||
@@ -422,17 +480,40 @@ def create_daily_schedule(user_uuid: str, med_id: str, base_times: List[str]):
|
||||
|
||||
|
||||
def mark_med_taken(user_uuid: str, med_id: str, scheduled_time):
|
||||
"""Mark a medication as taken."""
|
||||
"""Mark a medication schedule as taken."""
|
||||
_mark_med_status(user_uuid, med_id, scheduled_time, "taken")
|
||||
|
||||
|
||||
def mark_med_skipped(user_uuid: str, med_id: str, scheduled_time):
|
||||
"""Mark a medication schedule as skipped."""
|
||||
_mark_med_status(user_uuid, med_id, scheduled_time, "skipped")
|
||||
|
||||
|
||||
def _mark_med_status(user_uuid: str, med_id: str, scheduled_time, status: str):
|
||||
"""Update a medication schedule's status for today."""
|
||||
scheduled_time = _normalize_time(scheduled_time)
|
||||
today = user_today_for(user_uuid)
|
||||
|
||||
postgres.update(
|
||||
"medication_schedules",
|
||||
{"status": "taken"},
|
||||
{
|
||||
# Try matching by adjusted_time first
|
||||
where = {
|
||||
"user_uuid": user_uuid,
|
||||
"medication_id": med_id,
|
||||
"adjustment_date": today,
|
||||
}
|
||||
if scheduled_time is not None:
|
||||
where["adjusted_time"] = scheduled_time
|
||||
|
||||
schedules = postgres.select("medication_schedules", where)
|
||||
if schedules:
|
||||
postgres.update("medication_schedules", {"status": status}, {"id": schedules[0]["id"]})
|
||||
elif scheduled_time is not None:
|
||||
# Fallback: try matching by base_time (in case adjusted == base)
|
||||
where_base = {
|
||||
"user_uuid": user_uuid,
|
||||
"medication_id": med_id,
|
||||
"adjustment_date": today,
|
||||
"adjusted_time": scheduled_time,
|
||||
},
|
||||
)
|
||||
"base_time": scheduled_time,
|
||||
}
|
||||
schedules_base = postgres.select("medication_schedules", where_base)
|
||||
if schedules_base:
|
||||
postgres.update("medication_schedules", {"status": status}, {"id": schedules_base[0]["id"]})
|
||||
|
||||
Reference in New Issue
Block a user