Files
Synculous-2/api/routes/notifications.py
chelsea ba8c6e9050 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.
2026-02-15 00:48:43 -06:00

102 lines
3.6 KiB
Python

"""
Notifications API - Web push subscription management and VAPID key endpoint
"""
import os
import uuid
import flask
import jwt
import core.auth as auth
import core.postgres as postgres
import core.notifications as notifications
def _get_user_uuid(token):
try:
payload = jwt.decode(token, os.getenv("JWT_SECRET"), algorithms=["HS256"])
return payload.get("sub")
except (jwt.ExpiredSignatureError, jwt.InvalidTokenError):
return None
def _auth(request):
"""Extract and verify token. Returns user_uuid or None."""
header = request.headers.get("Authorization", "")
if not header.startswith("Bearer "):
return None
token = header[7:]
user_uuid = _get_user_uuid(token)
if not user_uuid or not auth.verifyLoginToken(token, userUUID=user_uuid):
return None
return user_uuid
def register(app):
@app.route("/api/notifications/vapid-public-key", methods=["GET"])
def api_vapidPublicKey():
"""Return the VAPID public key for push subscription."""
key = os.environ.get("VAPID_PUBLIC_KEY")
if not key:
return flask.jsonify({"error": "VAPID not configured"}), 503
return flask.jsonify({"public_key": key}), 200
@app.route("/api/notifications/subscribe", methods=["POST"])
def api_pushSubscribe():
"""Register a push subscription. Body: {endpoint, keys: {p256dh, auth}}"""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
data = flask.request.get_json()
if not data:
return flask.jsonify({"error": "missing body"}), 400
endpoint = data.get("endpoint")
keys = data.get("keys", {})
p256dh = keys.get("p256dh")
auth_key = keys.get("auth")
if not endpoint or not p256dh or not auth_key:
return flask.jsonify({"error": "missing endpoint or keys"}), 400
# Upsert: remove existing subscription with same endpoint for this user
existing = postgres.select("push_subscriptions", where={
"user_uuid": user_uuid,
"endpoint": endpoint,
})
if existing:
postgres.delete("push_subscriptions", {"id": existing[0]["id"]})
row = {
"id": str(uuid.uuid4()),
"user_uuid": user_uuid,
"endpoint": endpoint,
"p256dh": p256dh,
"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"])
def api_pushUnsubscribe():
"""Remove a push subscription. Body: {endpoint}"""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
data = flask.request.get_json()
if not data or not data.get("endpoint"):
return flask.jsonify({"error": "missing endpoint"}), 400
postgres.delete("push_subscriptions", {
"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