Fix cross-midnight adaptive dose creating false missed reminders

When adaptive timing shifts a late-night dose past midnight (e.g. 23:00
→ 00:42), the scheduler would create a new pending schedule on the next
day even if the dose was already taken. The proximity window was too
narrow to match the take log against the shifted time.

- Skip creating schedules for doses already taken/skipped (checks
  today + yesterday logs against base_time)
- Fix midnight wraparound in proximity check for should_send_nag
- Display base_time (actual dose time) in reminders instead of the
  internal adjusted_time

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 07:58:45 -06:00
parent 019561e7cd
commit fe07b3ebe7
2 changed files with 42 additions and 12 deletions

View File

@@ -395,9 +395,11 @@ def should_send_nag(
int(scheduled_time[:2]), int(scheduled_time[:2]),
int(scheduled_time[3:5]), int(scheduled_time[3:5]),
) )
diff_minutes = abs( log_mins = log_hour * 60 + log_min
(log_hour * 60 + log_min) - (sched_hour * 60 + sched_min) sched_mins = sched_hour * 60 + sched_min
) diff_minutes = abs(log_mins - sched_mins)
# Handle midnight wraparound (e.g. 23:00 vs 00:42)
diff_minutes = min(diff_minutes, 1440 - diff_minutes)
if diff_minutes <= proximity_window: if diff_minutes <= proximity_window:
return False, f"Already {action} today" return False, f"Already {action} today"
@@ -463,8 +465,33 @@ def create_daily_schedule(user_uuid: str, med_id: str, base_times: List[str], re
# Calculate adjusted times # Calculate adjusted times
adjusted_times = calculate_adjusted_times(user_uuid, base_times) adjusted_times = calculate_adjusted_times(user_uuid, base_times)
# Check recent med logs to skip doses already taken/skipped.
# Handles cross-midnight: if adaptive offset shifts 23:00 → 00:42 today,
# but the user already took the 23:00 dose last night, don't schedule it.
user_tz = tz_for_user(user_uuid)
yesterday = today - timedelta(days=1)
recent_logs = postgres.select("med_logs", {"medication_id": med_id, "user_uuid": user_uuid})
taken_base_times = set()
for log in recent_logs:
if log.get("action") not in ("taken", "skipped"):
continue
created_at = log.get("created_at")
if not created_at:
continue
if created_at.tzinfo is None:
created_at = created_at.replace(tzinfo=timezone.utc)
log_date = created_at.astimezone(user_tz).date()
if log_date not in (today, yesterday):
continue
log_sched = _normalize_time(log.get("scheduled_time"))
if log_sched:
taken_base_times.add(log_sched)
# Create schedule records for each time # Create schedule records for each time
for base_time, (adjusted_time, offset) in zip(base_times, adjusted_times): for base_time, (adjusted_time, offset) in zip(base_times, adjusted_times):
if base_time in taken_base_times:
continue
data = { data = {
"id": str(uuid.uuid4()), "id": str(uuid.uuid4()),
"user_uuid": user_uuid, "user_uuid": user_uuid,

View File

@@ -342,14 +342,20 @@ def check_adaptive_medication_reminders():
if already_handled: if already_handled:
continue continue
# Send notification # Send notification — display base_time to the user
base_display = sched.get("base_time")
if isinstance(base_display, time_type):
base_display = base_display.strftime("%H:%M")
elif base_display is not None:
base_display = str(base_display)[:5]
user_settings = notifications.getNotificationSettings(user_uuid) user_settings = notifications.getNotificationSettings(user_uuid)
if user_settings: if user_settings:
offset = sched.get("adjustment_minutes", 0) offset = sched.get("adjustment_minutes", 0)
if offset > 0: if offset > 0:
msg = f"⏰ Time to take {med['name']} ({med['dosage']} {med['unit']}) · {check_time} (adjusted +{offset}min)" msg = f"⏰ Time to take {med['name']} ({med['dosage']} {med['unit']}) · {base_display} (adjusted +{offset}min)"
else: else:
msg = f"⏰ Time to take {med['name']} ({med['dosage']} {med['unit']}) · {check_time}" msg = f"⏰ Time to take {med['name']} ({med['dosage']} {med['unit']}) · {base_display}"
notifications._sendToEnabledChannels( notifications._sendToEnabledChannels(
user_settings, msg, user_uuid=user_uuid user_settings, msg, user_uuid=user_uuid
@@ -443,12 +449,9 @@ def check_nagging():
if not should_nag: if not should_nag:
continue continue
# Get the time to display # Always display the base_time (the user's actual dose time),
adaptive_enabled = settings.get("adaptive_timing_enabled") # not the internal adjusted_time used for scheduling.
if adaptive_enabled: display_time = sched.get("base_time")
display_time = sched.get("adjusted_time")
else:
display_time = sched.get("base_time")
# Normalize TIME objects for display # Normalize TIME objects for display
if isinstance(display_time, time_type): if isinstance(display_time, time_type):
display_time = display_time.strftime("%H:%M") display_time = display_time.strftime("%H:%M")