From 3ccd11153c6dd427a1eea53d55bf160159d2df3c Mon Sep 17 00:00:00 2001 From: chelsea Date: Sat, 14 Feb 2026 20:06:39 -0600 Subject: [PATCH] bug fixes --- api/main.py | 40 ++++++++++++ api/routes/medications.py | 36 +++++++++-- api/routes/routines.py | 62 ++++++++++++------- core/routines.py | 6 +- .../app/dashboard/medications/new/page.tsx | 6 +- .../src/app/dashboard/medications/page.tsx | 19 ++++-- synculous-client/src/lib/api.ts | 1 + 7 files changed, 131 insertions(+), 39 deletions(-) diff --git a/api/main.py b/api/main.py index ba2d8f9..e3a2f15 100644 --- a/api/main.py +++ b/api/main.py @@ -155,8 +155,48 @@ def health_check(): return flask.jsonify({"status": "ok"}), 200 +def _seed_templates_if_empty(): + """Auto-seed routine templates if the table is empty.""" + try: + count = postgres.count("routine_templates") + if count == 0: + import logging + logging.getLogger(__name__).info("No templates found, seeding from seed_templates.sql...") + seed_path = os.path.join(os.path.dirname(__file__), "..", "config", "seed_templates.sql") + if os.path.exists(seed_path): + with open(seed_path, "r") as f: + sql = f.read() + with postgres.get_cursor() as cur: + cur.execute(sql) + logging.getLogger(__name__).info("Templates seeded successfully.") + except Exception as e: + import logging + logging.getLogger(__name__).warning(f"Failed to seed templates: {e}") + + +def _seed_rewards_if_empty(): + """Auto-seed reward pool if the table is empty.""" + try: + count = postgres.count("reward_pool") + if count == 0: + import logging + logging.getLogger(__name__).info("No rewards found, seeding from seed_rewards.sql...") + seed_path = os.path.join(os.path.dirname(__file__), "..", "config", "seed_rewards.sql") + if os.path.exists(seed_path): + with open(seed_path, "r") as f: + sql = f.read() + with postgres.get_cursor() as cur: + cur.execute(sql) + logging.getLogger(__name__).info("Rewards seeded successfully.") + except Exception as e: + import logging + logging.getLogger(__name__).warning(f"Failed to seed rewards: {e}") + + if __name__ == "__main__": for module in ROUTE_MODULES: if hasattr(module, "register"): module.register(app) + _seed_templates_if_empty() + _seed_rewards_if_empty() app.run(host="0.0.0.0", port=5000) diff --git a/api/routes/medications.py b/api/routes/medications.py index 1a5de12..da16445 100644 --- a/api/routes/medications.py +++ b/api/routes/medications.py @@ -342,18 +342,26 @@ def register(app): all_logs = postgres.select( "med_logs", - where={"medication_id": med["id"], "action": "taken"}, + where={"medication_id": med["id"]}, ) today_taken = [ log.get("scheduled_time", "") for log in all_logs - if str(log.get("created_at", ""))[:10] == today_str + if log.get("action") == "taken" + and str(log.get("created_at", ""))[:10] == today_str + ] + today_skipped = [ + log.get("scheduled_time", "") + for log in all_logs + if log.get("action") == "skipped" + and str(log.get("created_at", ""))[:10] == today_str ] result.append({ "medication": med, "scheduled_times": [] if is_prn else med.get("times", []), "taken_times": today_taken, + "skipped_times": today_skipped, "is_prn": is_prn, }) seen_med_ids.add(med["id"]) @@ -378,18 +386,26 @@ def register(app): all_logs = postgres.select( "med_logs", - where={"medication_id": med["id"], "action": "taken"}, + where={"medication_id": med["id"]}, ) tomorrow_taken = [ log.get("scheduled_time", "") for log in all_logs - if str(log.get("created_at", ""))[:10] == tomorrow_str + if log.get("action") == "taken" + and str(log.get("created_at", ""))[:10] == tomorrow_str + ] + tomorrow_skipped = [ + log.get("scheduled_time", "") + for log in all_logs + if log.get("action") == "skipped" + and str(log.get("created_at", ""))[:10] == tomorrow_str ] result.append({ "medication": med, "scheduled_times": early_times, "taken_times": tomorrow_taken, + "skipped_times": tomorrow_skipped, "is_prn": False, "is_next_day": True, }) @@ -415,18 +431,26 @@ def register(app): all_logs = postgres.select( "med_logs", - where={"medication_id": med["id"], "action": "taken"}, + where={"medication_id": med["id"]}, ) yesterday_taken = [ log.get("scheduled_time", "") for log in all_logs - if str(log.get("created_at", ""))[:10] == yesterday_str + if log.get("action") == "taken" + and str(log.get("created_at", ""))[:10] == yesterday_str + ] + yesterday_skipped = [ + log.get("scheduled_time", "") + for log in all_logs + if log.get("action") == "skipped" + and str(log.get("created_at", ""))[:10] == yesterday_str ] result.append({ "medication": med, "scheduled_times": late_times, "taken_times": yesterday_taken, + "skipped_times": yesterday_skipped, "is_prn": False, "is_previous_day": True, }) diff --git a/api/routes/routines.py b/api/routes/routines.py index f6a43bb..baad7a0 100644 --- a/api/routes/routines.py +++ b/api/routes/routines.py @@ -98,40 +98,56 @@ def _complete_session_with_celebration(session_id, user_uuid, session): else: duration_minutes = 0 - # Update session as completed with duration + # Update session as completed with duration — this MUST succeed postgres.update("routine_sessions", { "status": "completed", "completed_at": now.isoformat(), "actual_duration_minutes": int(duration_minutes), }, {"id": session_id}) - # Update streak (returns streak with optional 'milestone' key) - streak_result = routines_core._update_streak(user_uuid, session["routine_id"]) + # Gather celebration stats — failures here should not break completion + streak_current = 1 + streak_longest = 1 + streak_milestone = None + steps_completed = 0 + steps_skipped = 0 + total_completions = 1 - # Get streak data - streak = postgres.select_one("routine_streaks", { - "user_uuid": user_uuid, - "routine_id": session["routine_id"], - }) - streak_milestone = streak_result.get("milestone") if streak_result else None + try: + streak_result = routines_core._update_streak(user_uuid, session["routine_id"]) + streak = postgres.select_one("routine_streaks", { + "user_uuid": user_uuid, + "routine_id": session["routine_id"], + }) + if streak: + streak_current = streak["current_streak"] + streak_longest = streak["longest_streak"] + streak_milestone = streak_result.get("milestone") if streak_result else None + except Exception: + pass - # Count step results for this session - step_results = postgres.select("routine_step_results", {"session_id": session_id}) - steps_completed = sum(1 for r in step_results if r.get("result") == "completed") - steps_skipped = sum(1 for r in step_results if r.get("result") == "skipped") + try: + step_results = postgres.select("routine_step_results", {"session_id": session_id}) + steps_completed = sum(1 for r in step_results if r.get("result") == "completed") + steps_skipped = sum(1 for r in step_results if r.get("result") == "skipped") + except Exception: + pass - # Total completions for this routine - all_completed = postgres.select("routine_sessions", { - "routine_id": session["routine_id"], - "user_uuid": user_uuid, - "status": "completed", - }) + try: + all_completed = postgres.select("routine_sessions", { + "routine_id": session["routine_id"], + "user_uuid": user_uuid, + "status": "completed", + }) + total_completions = len(all_completed) + except Exception: + pass result = { - "streak_current": streak["current_streak"] if streak else 1, - "streak_longest": streak["longest_streak"] if streak else 1, + "streak_current": streak_current, + "streak_longest": streak_longest, "session_duration_minutes": duration_minutes, - "total_completions": len(all_completed), + "total_completions": total_completions, "steps_completed": steps_completed, "steps_skipped": steps_skipped, } @@ -339,6 +355,8 @@ def register(app): if not routine: return flask.jsonify({"error": "not found"}), 404 active = postgres.select_one("routine_sessions", {"user_uuid": user_uuid, "status": "active"}) + if not active: + active = postgres.select_one("routine_sessions", {"user_uuid": user_uuid, "status": "paused"}) if active: return flask.jsonify({"error": "already have active session", "session_id": active["id"]}), 409 steps = postgres.select( diff --git a/core/routines.py b/core/routines.py index ec85f0e..66f57fa 100644 --- a/core/routines.py +++ b/core/routines.py @@ -58,7 +58,7 @@ def pause_session(session_id, user_uuid): return {"error": "not_active"} result = postgres.update( "routine_sessions", - {"status": "paused", "paused_at": datetime.now().isoformat()}, + {"status": "paused", "paused_at": tz.user_now().isoformat()}, {"id": session_id} ) return result @@ -95,7 +95,7 @@ def abort_session(session_id, user_uuid, reason=None): { "status": "aborted", "abort_reason": reason or "Aborted by user", - "completed_at": datetime.now().isoformat() + "completed_at": tz.user_now().isoformat() }, {"id": session_id} ) @@ -111,7 +111,7 @@ def complete_session(session_id, user_uuid): if not session: return None - completed_at = datetime.now() + completed_at = tz.user_now() result = postgres.update( "routine_sessions", {"status": "completed", "completed_at": completed_at.isoformat()}, diff --git a/synculous-client/src/app/dashboard/medications/new/page.tsx b/synculous-client/src/app/dashboard/medications/new/page.tsx index f1e4cdb..b1c81f0 100644 --- a/synculous-client/src/app/dashboard/medications/new/page.tsx +++ b/synculous-client/src/app/dashboard/medications/new/page.tsx @@ -147,7 +147,6 @@ export default function NewMedicationPage() { className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none" > - @@ -205,7 +204,7 @@ export default function NewMedicationPage() { {/* Times picker — hidden for as_needed */} {frequency !== 'as_needed' && (
-
+
+ {frequency === 'daily' && ( +

Add multiple times for 2x, 3x, or more doses per day

+ )}
{times.map((time, index) => (
diff --git a/synculous-client/src/app/dashboard/medications/page.tsx b/synculous-client/src/app/dashboard/medications/page.tsx index cf901d1..99b64f9 100644 --- a/synculous-client/src/app/dashboard/medications/page.tsx +++ b/synculous-client/src/app/dashboard/medications/page.tsx @@ -32,6 +32,7 @@ interface TodaysMedication { }; scheduled_times: string[]; taken_times: string[]; + skipped_times?: string[]; is_prn?: boolean; is_next_day?: boolean; is_previous_day?: boolean; @@ -44,10 +45,11 @@ interface AdherenceEntry { is_prn?: boolean; } -type TimeStatus = 'overdue' | 'due_now' | 'upcoming' | 'taken'; +type TimeStatus = 'overdue' | 'due_now' | 'upcoming' | 'taken' | 'skipped'; -function getTimeStatus(scheduledTime: string, takenTimes: string[], now: Date): TimeStatus { +function getTimeStatus(scheduledTime: string, takenTimes: string[], skippedTimes: string[], now: Date): TimeStatus { if (takenTimes.includes(scheduledTime)) return 'taken'; + if (skippedTimes.includes(scheduledTime)) return 'skipped'; const [h, m] = scheduledTime.split(':').map(Number); const scheduled = new Date(now); @@ -69,7 +71,9 @@ const formatSchedule = (med: Medication): string => { return `Every ${med.interval_days} days`; } if (med.frequency === 'as_needed') return 'As needed'; - if (med.frequency === 'twice_daily') return 'Twice daily'; + const timesCount = med.times?.length || 0; + if (med.frequency === 'twice_daily' || timesCount === 2) return 'Twice daily'; + if (timesCount >= 3) return `${timesCount}x daily`; return 'Daily'; }; @@ -134,8 +138,8 @@ export default function MedicationsPage() { } for (const time of item.scheduled_times) { - const status = getTimeStatus(time, item.taken_times, now); - if (status === 'upcoming') { + const status = getTimeStatus(time, item.taken_times, item.skipped_times || [], now); + if (status === 'upcoming' || status === 'skipped') { upcoming.push({ item, time, status }); } else { due.push({ item, time, status }); @@ -144,7 +148,7 @@ export default function MedicationsPage() { } // Sort due: overdue first, then due_now, then by time - const statusOrder: Record = { overdue: 0, due_now: 1, taken: 2, upcoming: 3 }; + const statusOrder: Record = { overdue: 0, due_now: 1, taken: 2, skipped: 3, upcoming: 4 }; due.sort((a, b) => statusOrder[a.status] - statusOrder[b.status] || a.time.localeCompare(b.time)); upcoming.sort((a, b) => a.time.localeCompare(b.time)); @@ -197,6 +201,7 @@ export default function MedicationsPage() { if (status === 'overdue') return 'border-l-4 border-l-red-500'; if (status === 'due_now') return 'border-l-4 border-l-amber-500'; if (status === 'taken') return 'border-l-4 border-l-green-500'; + if (status === 'skipped') return 'border-l-4 border-l-gray-400'; return ''; }; @@ -245,6 +250,8 @@ export default function MedicationsPage() { Taken + ) : entry.status === 'skipped' ? ( + Skipped ) : (