Changes Made
1. config/schema.sql — Added timezone_name VARCHAR(100) to user_preferences table + an ALTER TABLE migration at the bottom for existing DBs. 2. core/tz.py — Rewrote with dual-path timezone support: - Request context: _get_request_tz() now checks X-Timezone-Name header (IANA) first, falls back to X-Timezone-Offset - Background jobs: New tz_for_user(user_uuid) and user_now_for(user_uuid) read stored timezone_name from prefs, fall back to numeric offset, then UTC - All existing function signatures (user_now(), user_today()) preserved for backward compat 3. scheduler/daemon.py — Fixed 3 bugs: - _user_now_for() now delegates to tz.user_now_for() which uses IANA timezone names (DST-safe) - check_nagging() — replaced datetime.utcnow() with _user_now_for(user_uuid) so nags evaluate in user's timezone - poll_callback() — replaced single UTC midnight check with _check_per_user_midnight_schedules() that iterates users and creates daily schedules at their local midnight 4. api/routes/preferences.py — Added "timezone_name" to allowed PUT fields. 5. synculous-client/src/lib/api.ts — Added X-Timezone-Name header to every request + added timezone_name to preferences update type. 6. synculous-client/src/app/dashboard/layout.tsx — Now syncs both timezone_offset and timezone_name (via Intl.DateTimeFormat().resolvedOptions().timeZone) on session start.
This commit is contained in:
@@ -13,6 +13,7 @@ import core.postgres as postgres
|
||||
import core.notifications as notifications
|
||||
import core.adaptive_meds as adaptive_meds
|
||||
import core.snitch as snitch
|
||||
import core.tz as tz
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -21,14 +22,8 @@ POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL", 60))
|
||||
|
||||
|
||||
def _user_now_for(user_uuid):
|
||||
"""Get current datetime in a user's timezone using their stored offset."""
|
||||
prefs = postgres.select_one("user_preferences", {"user_uuid": user_uuid})
|
||||
offset_minutes = 0
|
||||
if prefs and prefs.get("timezone_offset") is not None:
|
||||
offset_minutes = prefs["timezone_offset"]
|
||||
# JS getTimezoneOffset: positive = behind UTC, so negate
|
||||
tz_obj = timezone(timedelta(minutes=-offset_minutes))
|
||||
return datetime.now(tz_obj)
|
||||
"""Get current datetime in a user's timezone using their stored preferences."""
|
||||
return tz.user_now_for(user_uuid)
|
||||
|
||||
|
||||
def check_medication_reminders():
|
||||
@@ -158,7 +153,8 @@ def check_refills():
|
||||
|
||||
|
||||
def create_daily_adaptive_schedules():
|
||||
"""Create today's medication schedules with adaptive timing."""
|
||||
"""Create today's medication schedules with adaptive timing.
|
||||
Called per-user when it's midnight in their timezone."""
|
||||
try:
|
||||
from datetime import date as date_type
|
||||
|
||||
@@ -312,7 +308,7 @@ def check_nagging():
|
||||
logger.debug(f"Nagging disabled for user {user_uuid}")
|
||||
continue
|
||||
|
||||
now = datetime.utcnow()
|
||||
now = _user_now_for(user_uuid)
|
||||
today = now.date()
|
||||
|
||||
# Get today's schedules
|
||||
@@ -428,18 +424,55 @@ def check_nagging():
|
||||
logger.error(f"Error checking nags: {e}")
|
||||
|
||||
|
||||
def poll_callback():
|
||||
"""Called every POLL_INTERVAL seconds."""
|
||||
# Create daily schedules at midnight
|
||||
now = datetime.utcnow()
|
||||
if now.hour == 0 and now.minute < POLL_INTERVAL / 60:
|
||||
def _get_distinct_user_uuids():
|
||||
"""Return a set of user UUIDs that have active medications or routines."""
|
||||
uuids = set()
|
||||
try:
|
||||
meds = postgres.select("medications", where={"active": True})
|
||||
for m in meds:
|
||||
uid = m.get("user_uuid")
|
||||
if uid:
|
||||
uuids.add(uid)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
routines = postgres.select("routines")
|
||||
for r in routines:
|
||||
uid = r.get("user_uuid")
|
||||
if uid:
|
||||
uuids.add(uid)
|
||||
except Exception:
|
||||
pass
|
||||
return uuids
|
||||
|
||||
|
||||
def _check_per_user_midnight_schedules():
|
||||
"""Create daily adaptive schedules for each user when it's midnight in
|
||||
their timezone (within the poll window)."""
|
||||
for user_uuid in _get_distinct_user_uuids():
|
||||
try:
|
||||
create_daily_adaptive_schedules()
|
||||
now = _user_now_for(user_uuid)
|
||||
if now.hour == 0 and now.minute < POLL_INTERVAL / 60:
|
||||
user_meds = postgres.select(
|
||||
"medications", where={"user_uuid": user_uuid, "active": True}
|
||||
)
|
||||
for med in user_meds:
|
||||
times = med.get("times", [])
|
||||
if times:
|
||||
adaptive_meds.create_daily_schedule(
|
||||
user_uuid, med["id"], times
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Could not create adaptive schedules (tables may not exist): {e}"
|
||||
f"Could not create adaptive schedules for user {user_uuid}: {e}"
|
||||
)
|
||||
|
||||
|
||||
def poll_callback():
|
||||
"""Called every POLL_INTERVAL seconds."""
|
||||
# Create daily schedules per-user at their local midnight
|
||||
_check_per_user_midnight_schedules()
|
||||
|
||||
# Check reminders - use both original and adaptive checks
|
||||
logger.info("Checking medication reminders")
|
||||
check_medication_reminders()
|
||||
|
||||
Reference in New Issue
Block a user