From ba8c6e9050a1e550b6c15720c2a022c64c6813b1 Mon Sep 17 00:00:00 2001 From: chelsea Date: Sun, 15 Feb 2026 00:48:43 -0600 Subject: [PATCH] Here's a summary of all fixes: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #4 — Skip not clearing med File: synculous-client/src/app/dashboard/medications/page.tsx Root cause: Skipped medications were routed to the "Upcoming" section (status === 'skipped' in the upcoming condition), so they appeared as if still pending. Fix: Removed || status === 'skipped' from the grouping condition. Now skipped meds go to the "Due" section where they properly render with the "Skipped" label — same pattern as taken meds showing "Taken". Issue #5 — "Invested -359m in yourself" Files: api/routes/routines.py, synculous-client/src/app/dashboard/page.tsx, synculous-client/src/app/dashboard/stats/page.tsx Root cause: Session duration was calculated by comparing a naive UTC created_at from PostgreSQL with the user's local time (after stripping timezone). For users behind UTC (e.g., CST/UTC-6), this produced negative durations (~-359 minutes ≈ -6 hours offset). Fix: Added _make_aware_utc() helper that treats naive datetimes as UTC before comparison. Also clamped durations to max(0, ...) on both backend and frontend formatTime as a safety net. Issue #6 — Push notifications not working File: api/routes/notifications.py Root cause: Subscribing to push notifications created a push_subscriptions row but never set web_push_enabled: true in the notifications table. The scheduler daemon checks web_push_enabled before sending, so push notifications were always skipped. Fix: When subscribing, also call notifications.setNotificationSettings() to enable web_push_enabled. When unsubscribing (and no subscriptions remain), disable it. --- api/routes/notifications.py | 7 +++++ api/routes/routines.py | 28 +++++++++---------- .../src/app/dashboard/medications/page.tsx | 2 +- synculous-client/src/app/dashboard/page.tsx | 7 +++-- .../src/app/dashboard/stats/page.tsx | 7 +++-- 5 files changed, 30 insertions(+), 21 deletions(-) diff --git a/api/routes/notifications.py b/api/routes/notifications.py index 2b40219..ca76494 100644 --- a/api/routes/notifications.py +++ b/api/routes/notifications.py @@ -9,6 +9,7 @@ import flask import jwt import core.auth as auth import core.postgres as postgres +import core.notifications as notifications def _get_user_uuid(token): @@ -75,6 +76,8 @@ def register(app): "auth": auth_key, } postgres.insert("push_subscriptions", row) + # Ensure web_push_enabled is set in notifications settings + notifications.setNotificationSettings(user_uuid, {"web_push_enabled": True}) return flask.jsonify({"subscribed": True}), 201 @app.route("/api/notifications/subscribe", methods=["DELETE"]) @@ -91,4 +94,8 @@ def register(app): "user_uuid": user_uuid, "endpoint": data["endpoint"], }) + # If no subscriptions remain, disable web push + remaining = postgres.select("push_subscriptions", where={"user_uuid": user_uuid}) + if not remaining: + notifications.setNotificationSettings(user_uuid, {"web_push_enabled": False}) return flask.jsonify({"unsubscribed": True}), 200 diff --git a/api/routes/routines.py b/api/routes/routines.py index baad7a0..1a1fe97 100644 --- a/api/routes/routines.py +++ b/api/routes/routines.py @@ -35,6 +35,14 @@ def _auth(request): return user_uuid +def _make_aware_utc(dt): + """Ensure a datetime is timezone-aware; assume naive datetimes are UTC.""" + if dt.tzinfo is None: + from datetime import timezone as _tz + return dt.replace(tzinfo=_tz.utc) + return dt + + def _record_step_result(session_id, step_id, step_index, result, session): """Record a per-step result (completed or skipped).""" try: @@ -51,11 +59,8 @@ def _record_step_result(session_id, step_id, step_index, result, session): if last_completed: if isinstance(last_completed, str): last_completed = datetime.fromisoformat(last_completed) - # Make naive datetimes comparable with aware ones - if last_completed.tzinfo is None: - duration_seconds = int((now.replace(tzinfo=None) - last_completed).total_seconds()) - else: - duration_seconds = int((now - last_completed).total_seconds()) + last_completed = _make_aware_utc(last_completed) + duration_seconds = max(0, int((now - last_completed).total_seconds())) else: duration_seconds = None else: @@ -63,10 +68,8 @@ def _record_step_result(session_id, step_id, step_index, result, session): if created_at: if isinstance(created_at, str): created_at = datetime.fromisoformat(created_at) - if created_at.tzinfo is None: - duration_seconds = int((now.replace(tzinfo=None) - created_at).total_seconds()) - else: - duration_seconds = int((now - created_at).total_seconds()) + created_at = _make_aware_utc(created_at) + duration_seconds = max(0, int((now - created_at).total_seconds())) else: duration_seconds = None @@ -90,11 +93,8 @@ def _complete_session_with_celebration(session_id, user_uuid, session): if created_at: if isinstance(created_at, str): created_at = datetime.fromisoformat(created_at) - # Handle naive vs aware datetime comparison - if created_at.tzinfo is None: - duration_minutes = round((now.replace(tzinfo=None) - created_at).total_seconds() / 60, 1) - else: - duration_minutes = round((now - created_at).total_seconds() / 60, 1) + created_at = _make_aware_utc(created_at) + duration_minutes = max(0, round((now - created_at).total_seconds() / 60, 1)) else: duration_minutes = 0 diff --git a/synculous-client/src/app/dashboard/medications/page.tsx b/synculous-client/src/app/dashboard/medications/page.tsx index 99b64f9..5e46f2c 100644 --- a/synculous-client/src/app/dashboard/medications/page.tsx +++ b/synculous-client/src/app/dashboard/medications/page.tsx @@ -139,7 +139,7 @@ export default function MedicationsPage() { for (const time of item.scheduled_times) { const status = getTimeStatus(time, item.taken_times, item.skipped_times || [], now); - if (status === 'upcoming' || status === 'skipped') { + if (status === 'upcoming') { upcoming.push({ item, time, status }); } else { due.push({ item, time, status }); diff --git a/synculous-client/src/app/dashboard/page.tsx b/synculous-client/src/app/dashboard/page.tsx index 751d4cd..92c57a6 100644 --- a/synculous-client/src/app/dashboard/page.tsx +++ b/synculous-client/src/app/dashboard/page.tsx @@ -109,9 +109,10 @@ export default function DashboardPage() { }; const formatTime = (minutes: number) => { - if (minutes < 60) return `${minutes}m`; - const hours = Math.floor(minutes / 60); - const mins = minutes % 60; + const m = Math.max(0, minutes); + if (m < 60) return `${m}m`; + const hours = Math.floor(m / 60); + const mins = m % 60; return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`; }; diff --git a/synculous-client/src/app/dashboard/stats/page.tsx b/synculous-client/src/app/dashboard/stats/page.tsx index 155be30..6a2cdb2 100644 --- a/synculous-client/src/app/dashboard/stats/page.tsx +++ b/synculous-client/src/app/dashboard/stats/page.tsx @@ -105,9 +105,10 @@ export default function StatsPage() { }, [selectedRoutine]); const formatTime = (minutes: number) => { - if (minutes < 60) return `${minutes}m`; - const hours = Math.floor(minutes / 60); - const mins = minutes % 60; + const m = Math.max(0, minutes); + if (m < 60) return `${m}m`; + const hours = Math.floor(m / 60); + const mins = m % 60; return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`; };