Here's a summary of all fixes:
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.
This commit is contained in:
@@ -9,6 +9,7 @@ import flask
|
|||||||
import jwt
|
import jwt
|
||||||
import core.auth as auth
|
import core.auth as auth
|
||||||
import core.postgres as postgres
|
import core.postgres as postgres
|
||||||
|
import core.notifications as notifications
|
||||||
|
|
||||||
|
|
||||||
def _get_user_uuid(token):
|
def _get_user_uuid(token):
|
||||||
@@ -75,6 +76,8 @@ def register(app):
|
|||||||
"auth": auth_key,
|
"auth": auth_key,
|
||||||
}
|
}
|
||||||
postgres.insert("push_subscriptions", row)
|
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
|
return flask.jsonify({"subscribed": True}), 201
|
||||||
|
|
||||||
@app.route("/api/notifications/subscribe", methods=["DELETE"])
|
@app.route("/api/notifications/subscribe", methods=["DELETE"])
|
||||||
@@ -91,4 +94,8 @@ def register(app):
|
|||||||
"user_uuid": user_uuid,
|
"user_uuid": user_uuid,
|
||||||
"endpoint": data["endpoint"],
|
"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
|
return flask.jsonify({"unsubscribed": True}), 200
|
||||||
|
|||||||
@@ -35,6 +35,14 @@ def _auth(request):
|
|||||||
return user_uuid
|
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):
|
def _record_step_result(session_id, step_id, step_index, result, session):
|
||||||
"""Record a per-step result (completed or skipped)."""
|
"""Record a per-step result (completed or skipped)."""
|
||||||
try:
|
try:
|
||||||
@@ -51,11 +59,8 @@ def _record_step_result(session_id, step_id, step_index, result, session):
|
|||||||
if last_completed:
|
if last_completed:
|
||||||
if isinstance(last_completed, str):
|
if isinstance(last_completed, str):
|
||||||
last_completed = datetime.fromisoformat(last_completed)
|
last_completed = datetime.fromisoformat(last_completed)
|
||||||
# Make naive datetimes comparable with aware ones
|
last_completed = _make_aware_utc(last_completed)
|
||||||
if last_completed.tzinfo is None:
|
duration_seconds = max(0, int((now - last_completed).total_seconds()))
|
||||||
duration_seconds = int((now.replace(tzinfo=None) - last_completed).total_seconds())
|
|
||||||
else:
|
|
||||||
duration_seconds = int((now - last_completed).total_seconds())
|
|
||||||
else:
|
else:
|
||||||
duration_seconds = None
|
duration_seconds = None
|
||||||
else:
|
else:
|
||||||
@@ -63,10 +68,8 @@ def _record_step_result(session_id, step_id, step_index, result, session):
|
|||||||
if created_at:
|
if created_at:
|
||||||
if isinstance(created_at, str):
|
if isinstance(created_at, str):
|
||||||
created_at = datetime.fromisoformat(created_at)
|
created_at = datetime.fromisoformat(created_at)
|
||||||
if created_at.tzinfo is None:
|
created_at = _make_aware_utc(created_at)
|
||||||
duration_seconds = int((now.replace(tzinfo=None) - created_at).total_seconds())
|
duration_seconds = max(0, int((now - created_at).total_seconds()))
|
||||||
else:
|
|
||||||
duration_seconds = int((now - created_at).total_seconds())
|
|
||||||
else:
|
else:
|
||||||
duration_seconds = None
|
duration_seconds = None
|
||||||
|
|
||||||
@@ -90,11 +93,8 @@ def _complete_session_with_celebration(session_id, user_uuid, session):
|
|||||||
if created_at:
|
if created_at:
|
||||||
if isinstance(created_at, str):
|
if isinstance(created_at, str):
|
||||||
created_at = datetime.fromisoformat(created_at)
|
created_at = datetime.fromisoformat(created_at)
|
||||||
# Handle naive vs aware datetime comparison
|
created_at = _make_aware_utc(created_at)
|
||||||
if created_at.tzinfo is None:
|
duration_minutes = max(0, round((now - created_at).total_seconds() / 60, 1))
|
||||||
duration_minutes = round((now.replace(tzinfo=None) - created_at).total_seconds() / 60, 1)
|
|
||||||
else:
|
|
||||||
duration_minutes = round((now - created_at).total_seconds() / 60, 1)
|
|
||||||
else:
|
else:
|
||||||
duration_minutes = 0
|
duration_minutes = 0
|
||||||
|
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export default function MedicationsPage() {
|
|||||||
|
|
||||||
for (const time of item.scheduled_times) {
|
for (const time of item.scheduled_times) {
|
||||||
const status = getTimeStatus(time, item.taken_times, item.skipped_times || [], now);
|
const status = getTimeStatus(time, item.taken_times, item.skipped_times || [], now);
|
||||||
if (status === 'upcoming' || status === 'skipped') {
|
if (status === 'upcoming') {
|
||||||
upcoming.push({ item, time, status });
|
upcoming.push({ item, time, status });
|
||||||
} else {
|
} else {
|
||||||
due.push({ item, time, status });
|
due.push({ item, time, status });
|
||||||
|
|||||||
@@ -109,9 +109,10 @@ export default function DashboardPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formatTime = (minutes: number) => {
|
const formatTime = (minutes: number) => {
|
||||||
if (minutes < 60) return `${minutes}m`;
|
const m = Math.max(0, minutes);
|
||||||
const hours = Math.floor(minutes / 60);
|
if (m < 60) return `${m}m`;
|
||||||
const mins = minutes % 60;
|
const hours = Math.floor(m / 60);
|
||||||
|
const mins = m % 60;
|
||||||
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
|
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -105,9 +105,10 @@ export default function StatsPage() {
|
|||||||
}, [selectedRoutine]);
|
}, [selectedRoutine]);
|
||||||
|
|
||||||
const formatTime = (minutes: number) => {
|
const formatTime = (minutes: number) => {
|
||||||
if (minutes < 60) return `${minutes}m`;
|
const m = Math.max(0, minutes);
|
||||||
const hours = Math.floor(minutes / 60);
|
if (m < 60) return `${m}m`;
|
||||||
const mins = minutes % 60;
|
const hours = Math.floor(m / 60);
|
||||||
|
const mins = m % 60;
|
||||||
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
|
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user