From cc1aace73dec315bdd2a585678b585bc39666307 Mon Sep 17 00:00:00 2001 From: chelsea Date: Thu, 19 Feb 2026 20:31:43 -0600 Subject: [PATCH] Fix presence tracking and med reminder bugs --- bot/bot.py | 78 ++++++++++++++----- core/adaptive_meds.py | 57 +++++++++++--- .../src/app/dashboard/settings/page.tsx | 41 +++++++--- 3 files changed, 136 insertions(+), 40 deletions(-) diff --git a/bot/bot.py b/bot/bot.py index 0f231d5..d44b6e5 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -128,6 +128,7 @@ class JurySystem: async def retrieve(self, query_text, top_k=5): """Async retrieval — returns list of {metadata, score} dicts.""" import asyncio + return await asyncio.to_thread(self._retrieve_sync, query_text, top_k) async def query(self, query_text): @@ -147,6 +148,7 @@ If the answer is not in the context, say you don't know based on the provided te Be concise, compassionate, and practical.""" from ai.jury_council import generate_rag_answer + return await generate_rag_answer(query_text, context_text, system_prompt) except Exception as e: return f"Error querying DBT knowledge base: {e}" @@ -181,7 +183,9 @@ def apiRequest(method, endpoint, token=None, data=None, _retried=False): if resp.status_code == 401 and not _retried: new_token = _try_refresh_token_for_session(token) if new_token: - return apiRequest(method, endpoint, token=new_token, data=data, _retried=True) + return apiRequest( + method, endpoint, token=new_token, data=data, _retried=True + ) try: return resp.json(), resp.status_code except ValueError: @@ -201,9 +205,12 @@ def _try_refresh_token_for_session(expired_token): if cached: refresh_token = cached.get("refresh_token") if refresh_token: - result, status = apiRequest("post", "/api/refresh", - data={"refresh_token": refresh_token}, - _retried=True) + result, status = apiRequest( + "post", + "/api/refresh", + data={"refresh_token": refresh_token}, + _retried=True, + ) if status == 200 and "token" in result: new_token = result["token"] session["token"] = new_token @@ -258,7 +265,8 @@ def negotiateToken(discord_id, username, password): # Try refresh token first (avoids sending password) if cached and cached.get("refresh_token"): result, status = apiRequest( - "post", "/api/refresh", + "post", + "/api/refresh", data={"refresh_token": cached["refresh_token"]}, _retried=True, ) @@ -279,7 +287,9 @@ def negotiateToken(discord_id, username, password): and cached.get("hashed_password") and verifyPassword(password, cached.get("hashed_password")) ): - result, status = apiRequest("post", "/api/login", data=login_data, _retried=True) + result, status = apiRequest( + "post", "/api/login", data=login_data, _retried=True + ) if status == 200 and "token" in result: token = result["token"] payload = decodeJwtPayload(token) @@ -530,7 +540,9 @@ async def handleDBTQuery(message): if not jury_result.safe_questions: return True - await message.channel.send("🔍 Searching knowledge base with approved questions...") + await message.channel.send( + "🔍 Searching knowledge base with approved questions..." + ) # Step 3: Multi-query retrieval — deduplicated by chunk ID seen_ids = set() @@ -544,7 +556,9 @@ async def handleDBTQuery(message): context_chunks.append(r["metadata"]["text"]) if not context_chunks: - await message.channel.send("⚠️ No relevant content found in the knowledge base.") + await message.channel.send( + "⚠️ No relevant content found in the knowledge base." + ) return True context = "\n\n---\n\n".join(context_chunks) @@ -644,7 +658,8 @@ def _restore_sessions_from_cache(): if not refresh_token: continue result, status = apiRequest( - "post", "/api/refresh", + "post", + "/api/refresh", data={"refresh_token": refresh_token}, _retried=True, ) @@ -705,15 +720,20 @@ async def update_presence_tracking(): import core.adaptive_meds as adaptive_meds import core.postgres as postgres - print(f"[DEBUG] Running presence tracking. Guilds: {len(client.guilds)}", flush=True) + print( + f"[DEBUG] Running presence tracking. Guilds: {len(client.guilds)}", + flush=True, + ) for guild in client.guilds: - print(f"[DEBUG] Guild: {guild.name} ({guild.id}) - Members: {guild.member_count}") + print( + f"[DEBUG] Guild: {guild.name} ({guild.id}) - Members: {guild.member_count}" + ) # Get all users with presence tracking enabled settings = postgres.select( "adaptive_med_settings", {"presence_tracking_enabled": True} ) - + print(f"[DEBUG] Found {len(settings)} users with presence tracking enabled") for setting in settings: @@ -733,27 +753,46 @@ async def update_presence_tracking(): # Get the member from a shared guild (needed for presence data) try: member = None - target_id = int(discord_user_id) - + try: + target_id = int(discord_user_id) + except (ValueError, TypeError): + print( + f"[DEBUG] Invalid Discord ID for user {user_uuid}: {discord_user_id}", + flush=True, + ) + continue + # Search through all guilds the bot is in for guild in client.guilds: member = guild.get_member(target_id) - print(f"[DEBUG] Checked guild {guild.name}, member: {member}", flush=True) + print( + f"[DEBUG] Checked guild {guild.name}, member: {member}", + flush=True, + ) if member: break - + if not member: - print(f"[DEBUG] User {discord_user_id} not found in any shared guild", flush=True) + print( + f"[DEBUG] User {discord_user_id} not found in any shared guild", + flush=True, + ) continue # Check if user is online is_online = member.status != discord.Status.offline - print(f"[DEBUG] User status: {member.status}, is_online: {is_online}", flush=True) + print( + f"[DEBUG] User status: {member.status}, is_online: {is_online}", + flush=True, + ) # Get current presence from DB presence = adaptive_meds.get_user_presence(user_uuid) was_online = presence.get("is_currently_online") if presence else False - print(f"[DEBUG] Previous state: {was_online}, Current: {is_online}", flush=True) + print( + f"[DEBUG] Previous state: {was_online}, Current: {is_online}", + flush=True, + ) # Update presence if changed if is_online != was_online: @@ -788,6 +827,7 @@ async def presenceTrackingLoop(): except Exception as e: print(f"[ERROR] presenceTrackingLoop failed: {e}", flush=True) import traceback + traceback.print_exc() diff --git a/core/adaptive_meds.py b/core/adaptive_meds.py index 9364f65..cdb71d0 100644 --- a/core/adaptive_meds.py +++ b/core/adaptive_meds.py @@ -270,7 +270,7 @@ def should_send_nag( return False, "User offline" # Get today's schedule record for this specific time slot - today = current_time.date() + today = user_today_for(user_uuid) query = {"user_uuid": user_uuid, "medication_id": med_id, "adjustment_date": today} if scheduled_time is not None: query["adjusted_time"] = scheduled_time @@ -304,19 +304,56 @@ def should_send_nag( "medication_id": med_id, "user_uuid": user_uuid, "action": "taken", - "scheduled_time": scheduled_time, }, ) - # Filter to today's logs for this time slot - today_logs = [ - log - for log in logs - if log.get("created_at") and log["created_at"].date() == today - ] + # Get medication times to calculate dose interval for proximity check + med = postgres.select_one("medications", {"id": med_id}) + dose_interval_minutes = 60 # default fallback + if med and med.get("times"): + times = med["times"] + if len(times) >= 2: + time_minutes = [] + for t in times: + t = _normalize_time(t) + if t: + h, m = int(t[:2]), int(t[3:5]) + time_minutes.append(h * 60 + m) + time_minutes.sort() + intervals = [] + for i in range(1, len(time_minutes)): + intervals.append(time_minutes[i] - time_minutes[i - 1]) + if intervals: + dose_interval_minutes = min(intervals) - if today_logs: - return False, "Already taken today" + proximity_window = max(30, dose_interval_minutes // 2) + + # Filter to today's logs and check for this specific dose + for log in logs: + created_at = log.get("created_at") + if not created_at: + continue + if created_at.date() != today: + continue + + log_scheduled_time = log.get("scheduled_time") + if log_scheduled_time: + log_scheduled_time = _normalize_time(log_scheduled_time) + if log_scheduled_time == scheduled_time: + return False, "Already taken today" + else: + if scheduled_time and created_at: + log_hour = created_at.hour + log_min = created_at.minute + sched_hour, sched_min = ( + int(scheduled_time[:2]), + int(scheduled_time[3:5]), + ) + diff_minutes = abs( + (log_hour * 60 + log_min) - (sched_hour * 60 + sched_min) + ) + if diff_minutes <= proximity_window: + return False, "Already taken today" return True, "Time to nag" diff --git a/synculous-client/src/app/dashboard/settings/page.tsx b/synculous-client/src/app/dashboard/settings/page.tsx index 5ce5010..f39359b 100644 --- a/synculous-client/src/app/dashboard/settings/page.tsx +++ b/synculous-client/src/app/dashboard/settings/page.tsx @@ -396,14 +396,24 @@ export default function SettingsPage() { {notif.discord_enabled && ( - setNotif({ ...notif, discord_user_id: e.target.value })} - onBlur={() => updateNotif({ discord_user_id: notif.discord_user_id })} - className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 bg-white dark:bg-gray-700" - /> +
+ { + const val = e.target.value; + if (val === '' || /^\d+$/.test(val)) { + setNotif({ ...notif, discord_user_id: val }); + } + }} + onBlur={() => updateNotif({ discord_user_id: notif.discord_user_id })} + className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 bg-white dark:bg-gray-700" + /> +

+ Enable Developer Mode in Discord, right-click your profile, and copy User ID +

+
)} @@ -831,7 +841,7 @@ export default function SettingsPage() { /> setNewContact({ ...newContact, contact_value: e.target.value })} + onChange={(e) => { + const val = e.target.value; + if (newContact.contact_type === 'discord') { + if (val === '' || /^\d+$/.test(val)) { + setNewContact({ ...newContact, contact_value: val }); + } + } else { + setNewContact({ ...newContact, contact_value: val }); + } + }} className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-800" />