Files
Synculous-2/api/routes/preferences.py
chelsea 80ebecf0b1 Changes Made
1. config/schema.sql — Added timezone_name VARCHAR(100) to user_preferences table + an ALTER TABLE migration at the bottom for existing DBs.
  2. core/tz.py — Rewrote with dual-path timezone support:
  - Request context: _get_request_tz() now checks X-Timezone-Name header (IANA) first, falls back to X-Timezone-Offset
  - Background jobs: New tz_for_user(user_uuid) and user_now_for(user_uuid) read stored timezone_name from prefs, fall back to numeric offset, then UTC
  - All existing function signatures (user_now(), user_today()) preserved for backward compat

  3. scheduler/daemon.py — Fixed 3 bugs:
  - _user_now_for() now delegates to tz.user_now_for() which uses IANA timezone names (DST-safe)
  - check_nagging() — replaced datetime.utcnow() with _user_now_for(user_uuid) so nags evaluate in user's timezone
  - poll_callback() — replaced single UTC midnight check with _check_per_user_midnight_schedules() that iterates users and creates daily schedules at their local midnight

  4. api/routes/preferences.py — Added "timezone_name" to allowed PUT fields.

  5. synculous-client/src/lib/api.ts — Added X-Timezone-Name header to every request + added timezone_name to preferences update type.

  6. synculous-client/src/app/dashboard/layout.tsx — Now syncs both timezone_offset and timezone_name (via Intl.DateTimeFormat().resolvedOptions().timeZone) on session start.
2026-02-17 18:02:07 -06:00

75 lines
2.5 KiB
Python

"""
Preferences API - user settings
"""
import os
import uuid
import flask
import jwt
import core.auth as auth
import core.postgres as postgres
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):
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/preferences", methods=["GET"])
def api_getPreferences():
"""Get user preferences."""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
prefs = postgres.select_one("user_preferences", {"user_uuid": user_uuid})
if not prefs:
# Return defaults
return flask.jsonify({
"sound_enabled": False,
"haptic_enabled": True,
"show_launch_screen": True,
"celebration_style": "standard",
}), 200
return flask.jsonify(prefs), 200
@app.route("/api/preferences", methods=["PUT"])
def api_updatePreferences():
"""Update user preferences."""
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
allowed = ["sound_enabled", "haptic_enabled", "show_launch_screen", "celebration_style", "timezone_offset", "timezone_name"]
updates = {k: v for k, v in data.items() if k in allowed}
if not updates:
return flask.jsonify({"error": "no valid fields"}), 400
existing = postgres.select_one("user_preferences", {"user_uuid": user_uuid})
if existing:
result = postgres.update("user_preferences", updates, {"user_uuid": user_uuid})
return flask.jsonify(result[0] if result else {}), 200
else:
updates["id"] = str(uuid.uuid4())
updates["user_uuid"] = user_uuid
result = postgres.insert("user_preferences", updates)
return flask.jsonify(result), 201