""" Routines API - routine management Routines have ordered steps. Users start sessions to walk through them. """ 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): """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): # ── Routines CRUD ───────────────────────────────────────────── @app.route("/api/routines", methods=["GET"]) def api_listRoutines(): """List all routines for the logged-in user.""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 routines = postgres.select("routines", where={"user_uuid": user_uuid}, order_by="name") return flask.jsonify(routines), 200 @app.route("/api/routines", methods=["POST"]) def api_createRoutine(): """Create a new routine. Body: {name, description?, icon?}""" 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 if not data.get("name"): return flask.jsonify({"error": "missing required field: name"}), 400 data["id"] = str(uuid.uuid4()) data["user_uuid"] = user_uuid routine = postgres.insert("routines", data) return flask.jsonify(routine), 201 @app.route("/api/routines/", methods=["GET"]) def api_getRoutine(routine_id): """Get a routine with its steps.""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 routine = postgres.select_one("routines", {"id": routine_id, "user_uuid": user_uuid}) if not routine: return flask.jsonify({"error": "not found"}), 404 steps = postgres.select( "routine_steps", where={"routine_id": routine_id}, order_by="position", ) return flask.jsonify({"routine": routine, "steps": steps}), 200 @app.route("/api/routines/", methods=["PUT"]) def api_updateRoutine(routine_id): """Update routine details. Body: {name?, description?, icon?}""" 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 existing = postgres.select_one("routines", {"id": routine_id, "user_uuid": user_uuid}) if not existing: return flask.jsonify({"error": "not found"}), 404 allowed = ["name", "description", "icon"] updates = {k: v for k, v in data.items() if k in allowed} if not updates: return flask.jsonify({"error": "no valid fields to update"}), 400 result = postgres.update("routines", updates, {"id": routine_id, "user_uuid": user_uuid}) return flask.jsonify(result[0] if result else {}), 200 @app.route("/api/routines/", methods=["DELETE"]) def api_deleteRoutine(routine_id): """Delete a routine and all its steps/sessions.""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 existing = postgres.select_one("routines", {"id": routine_id, "user_uuid": user_uuid}) if not existing: return flask.jsonify({"error": "not found"}), 404 postgres.delete("routine_sessions", {"routine_id": routine_id}) postgres.delete("routine_steps", {"routine_id": routine_id}) postgres.delete("routine_schedules", {"routine_id": routine_id}) postgres.delete("routines", {"id": routine_id, "user_uuid": user_uuid}) return flask.jsonify({"deleted": True}), 200 # ── Steps CRUD ──────────────────────────────────────────────── @app.route("/api/routines//steps", methods=["GET"]) def api_listSteps(routine_id): """List steps for a routine, ordered by position.""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 routine = postgres.select_one("routines", {"id": routine_id, "user_uuid": user_uuid}) if not routine: return flask.jsonify({"error": "not found"}), 404 steps = postgres.select( "routine_steps", where={"routine_id": routine_id}, order_by="position", ) return flask.jsonify(steps), 200 @app.route("/api/routines//steps", methods=["POST"]) def api_addStep(routine_id): """Add a step to a routine. Body: {name, duration_minutes?, position?}""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 routine = postgres.select_one("routines", {"id": routine_id, "user_uuid": user_uuid}) if not routine: return flask.jsonify({"error": "not found"}), 404 data = flask.request.get_json() if not data: return flask.jsonify({"error": "missing body"}), 400 if not data.get("name"): return flask.jsonify({"error": "missing required field: name"}), 400 max_pos = postgres.select( "routine_steps", where={"routine_id": routine_id}, order_by="position DESC", limit=1, ) next_pos = (max_pos[0]["position"] + 1) if max_pos else 1 step = { "id": str(uuid.uuid4()), "routine_id": routine_id, "name": data["name"], "duration_minutes": data.get("duration_minutes"), "position": data.get("position", next_pos), } result = postgres.insert("routine_steps", step) return flask.jsonify(result), 201 @app.route("/api/routines//steps/", methods=["PUT"]) def api_updateStep(routine_id, step_id): """Update a step. Body: {name?, duration_minutes?, position?}""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 routine = postgres.select_one("routines", {"id": routine_id, "user_uuid": user_uuid}) if not routine: return flask.jsonify({"error": "not found"}), 404 data = flask.request.get_json() if not data: return flask.jsonify({"error": "missing body"}), 400 existing = postgres.select_one("routine_steps", {"id": step_id, "routine_id": routine_id}) if not existing: return flask.jsonify({"error": "step not found"}), 404 allowed = ["name", "duration_minutes", "position"] updates = {k: v for k, v in data.items() if k in allowed} if not updates: return flask.jsonify({"error": "no valid fields to update"}), 400 result = postgres.update("routine_steps", updates, {"id": step_id, "routine_id": routine_id}) return flask.jsonify(result[0] if result else {}), 200 @app.route("/api/routines//steps/", methods=["DELETE"]) def api_deleteStep(routine_id, step_id): """Delete a step from a routine.""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 routine = postgres.select_one("routines", {"id": routine_id, "user_uuid": user_uuid}) if not routine: return flask.jsonify({"error": "not found"}), 404 existing = postgres.select_one("routine_steps", {"id": step_id, "routine_id": routine_id}) if not existing: return flask.jsonify({"error": "step not found"}), 404 postgres.delete("routine_steps", {"id": step_id, "routine_id": routine_id}) return flask.jsonify({"deleted": True}), 200 @app.route("/api/routines//steps/reorder", methods=["PUT"]) def api_reorderSteps(routine_id): """Reorder steps. Body: {step_ids: [ordered list of step UUIDs]}""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 routine = postgres.select_one("routines", {"id": routine_id, "user_uuid": user_uuid}) if not routine: return flask.jsonify({"error": "not found"}), 404 data = flask.request.get_json() if not data or not data.get("step_ids"): return flask.jsonify({"error": "missing step_ids"}), 400 step_ids = data["step_ids"] for i, step_id in enumerate(step_ids): postgres.update("routine_steps", {"position": i + 1}, {"id": step_id, "routine_id": routine_id}) steps = postgres.select( "routine_steps", where={"routine_id": routine_id}, order_by="position", ) return flask.jsonify(steps), 200 # ── Routine Sessions (active run-through) ───────────────────── @app.route("/api/routines//start", methods=["POST"]) def api_startRoutine(routine_id): """Start a routine session. Returns the session with first step.""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 routine = postgres.select_one("routines", {"id": routine_id, "user_uuid": user_uuid}) if not routine: return flask.jsonify({"error": "not found"}), 404 active = postgres.select_one("routine_sessions", {"user_uuid": user_uuid, "status": "active"}) if active: return flask.jsonify({"error": "already have active session", "session_id": active["id"]}), 409 steps = postgres.select( "routine_steps", where={"routine_id": routine_id}, order_by="position", ) if not steps: return flask.jsonify({"error": "no steps in routine"}), 400 session = { "id": str(uuid.uuid4()), "routine_id": routine_id, "user_uuid": user_uuid, "status": "active", "current_step_index": 0, } result = postgres.insert("routine_sessions", session) return flask.jsonify({"session": result, "current_step": steps[0]}), 201 @app.route("/api/sessions/active", methods=["GET"]) def api_getActiveSession(): """Get the user's currently active routine session, if any.""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 session = postgres.select_one("routine_sessions", {"user_uuid": user_uuid, "status": "active"}) if not session: return flask.jsonify({"error": "no active session"}), 404 routine = postgres.select_one("routines", {"id": session["routine_id"]}) steps = postgres.select( "routine_steps", where={"routine_id": session["routine_id"]}, order_by="position", ) current_step = steps[session["current_step_index"]] if session["current_step_index"] < len(steps) else None return flask.jsonify({"session": session, "routine": routine, "current_step": current_step}), 200 @app.route("/api/sessions//complete-step", methods=["POST"]) def api_completeStep(session_id): """Mark current step done, advance to next. Body: {step_id}""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 session = postgres.select_one("routine_sessions", {"id": session_id, "user_uuid": user_uuid}) if not session: return flask.jsonify({"error": "not found"}), 404 if session["status"] != "active": return flask.jsonify({"error": "session not active"}), 400 data = flask.request.get_json() or {} steps = postgres.select( "routine_steps", where={"routine_id": session["routine_id"]}, order_by="position", ) next_index = session["current_step_index"] + 1 if next_index >= len(steps): postgres.update("routine_sessions", {"status": "completed"}, {"id": session_id}) return flask.jsonify({"session": {"status": "completed"}, "next_step": None}), 200 postgres.update("routine_sessions", {"current_step_index": next_index}, {"id": session_id}) return flask.jsonify({"session": {"current_step_index": next_index}, "next_step": steps[next_index]}), 200 @app.route("/api/sessions//skip-step", methods=["POST"]) def api_skipStep(session_id): """Skip current step, advance to next. Body: {step_id}""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 session = postgres.select_one("routine_sessions", {"id": session_id, "user_uuid": user_uuid}) if not session: return flask.jsonify({"error": "not found"}), 404 if session["status"] != "active": return flask.jsonify({"error": "session not active"}), 400 steps = postgres.select( "routine_steps", where={"routine_id": session["routine_id"]}, order_by="position", ) next_index = session["current_step_index"] + 1 if next_index >= len(steps): postgres.update("routine_sessions", {"status": "completed"}, {"id": session_id}) return flask.jsonify({"session": {"status": "completed"}, "next_step": None}), 200 postgres.update("routine_sessions", {"current_step_index": next_index}, {"id": session_id}) return flask.jsonify({"session": {"current_step_index": next_index}, "next_step": steps[next_index]}), 200 @app.route("/api/sessions//cancel", methods=["POST"]) def api_cancelSession(session_id): """Cancel an active routine session.""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 session = postgres.select_one("routine_sessions", {"id": session_id, "user_uuid": user_uuid}) if not session: return flask.jsonify({"error": "not found"}), 404 postgres.update("routine_sessions", {"status": "cancelled"}, {"id": session_id}) return flask.jsonify({"session": {"status": "cancelled"}}), 200 # ── Routine History / Stats ─────────────────────────────────── @app.route("/api/routines//history", methods=["GET"]) def api_routineHistory(routine_id): """Get past sessions for a routine. Query: ?days=7""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 routine = postgres.select_one("routines", {"id": routine_id, "user_uuid": user_uuid}) if not routine: return flask.jsonify({"error": "not found"}), 404 days = flask.request.args.get("days", 7, type=int) sessions = postgres.select( "routine_sessions", where={"routine_id": routine_id}, order_by="created_at DESC", limit=days, ) return flask.jsonify(sessions), 200 # ── Routine Scheduling ──────────────────────────────────────── @app.route("/api/routines//schedule", methods=["PUT"]) def api_setRoutineSchedule(routine_id): """Set when this routine should run. Body: {days: ["mon","tue",...], time: "08:00", remind: true}""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 routine = postgres.select_one("routines", {"id": routine_id, "user_uuid": user_uuid}) if not routine: return flask.jsonify({"error": "not found"}), 404 data = flask.request.get_json() if not data: return flask.jsonify({"error": "missing body"}), 400 existing = postgres.select_one("routine_schedules", {"routine_id": routine_id}) schedule_data = { "routine_id": routine_id, "days": data.get("days", []), "time": data.get("time"), "remind": data.get("remind", True), } if existing: result = postgres.update("routine_schedules", schedule_data, {"routine_id": routine_id}) return flask.jsonify(result[0] if result else {}), 200 else: schedule_data["id"] = str(uuid.uuid4()) result = postgres.insert("routine_schedules", schedule_data) return flask.jsonify(result), 201 @app.route("/api/routines//schedule", methods=["GET"]) def api_getRoutineSchedule(routine_id): """Get the schedule for a routine.""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 routine = postgres.select_one("routines", {"id": routine_id, "user_uuid": user_uuid}) if not routine: return flask.jsonify({"error": "not found"}), 404 schedule = postgres.select_one("routine_schedules", {"routine_id": routine_id}) if not schedule: return flask.jsonify({"error": "no schedule set"}), 404 return flask.jsonify(schedule), 200 @app.route("/api/routines//schedule", methods=["DELETE"]) def api_deleteRoutineSchedule(routine_id): """Remove the schedule from a routine.""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 routine = postgres.select_one("routines", {"id": routine_id, "user_uuid": user_uuid}) if not routine: return flask.jsonify({"error": "not found"}), 404 postgres.delete("routine_schedules", {"routine_id": routine_id}) return flask.jsonify({"deleted": True}), 200