First synculous 2 Big-Pickle pass.
This commit is contained in:
@@ -3,6 +3,7 @@ Medications API - medication scheduling, logging, and adherence tracking
|
||||
"""
|
||||
|
||||
import os
|
||||
import uuid
|
||||
import flask
|
||||
import jwt
|
||||
import core.auth as auth
|
||||
@@ -39,7 +40,8 @@ def register(app):
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
pass
|
||||
meds = postgres.select("medications", where={"user_uuid": user_uuid}, order_by="name")
|
||||
return flask.jsonify(meds), 200
|
||||
|
||||
@app.route("/api/medications", methods=["POST"])
|
||||
def api_addMedication():
|
||||
@@ -48,7 +50,18 @@ def register(app):
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
data = flask.request.get_json()
|
||||
pass
|
||||
if not data:
|
||||
return flask.jsonify({"error": "missing body"}), 400
|
||||
required = ["name", "dosage", "unit", "frequency"]
|
||||
missing = [f for f in required if not data.get(f)]
|
||||
if missing:
|
||||
return flask.jsonify({"error": f"missing required fields: {', '.join(missing)}"}), 400
|
||||
data["id"] = str(uuid.uuid4())
|
||||
data["user_uuid"] = user_uuid
|
||||
data["times"] = data.get("times", [])
|
||||
data["active"] = True
|
||||
med = postgres.insert("medications", data)
|
||||
return flask.jsonify(med), 201
|
||||
|
||||
@app.route("/api/medications/<med_id>", methods=["GET"])
|
||||
def api_getMedication(med_id):
|
||||
@@ -56,7 +69,10 @@ def register(app):
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
pass
|
||||
med = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid})
|
||||
if not med:
|
||||
return flask.jsonify({"error": "not found"}), 404
|
||||
return flask.jsonify(med), 200
|
||||
|
||||
@app.route("/api/medications/<med_id>", methods=["PUT"])
|
||||
def api_updateMedication(med_id):
|
||||
@@ -65,7 +81,17 @@ def register(app):
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
data = flask.request.get_json()
|
||||
pass
|
||||
if not data:
|
||||
return flask.jsonify({"error": "missing body"}), 400
|
||||
existing = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid})
|
||||
if not existing:
|
||||
return flask.jsonify({"error": "not found"}), 404
|
||||
allowed = ["name", "dosage", "unit", "frequency", "times", "notes", "active"]
|
||||
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("medications", updates, {"id": med_id, "user_uuid": user_uuid})
|
||||
return flask.jsonify(result[0] if result else {}), 200
|
||||
|
||||
@app.route("/api/medications/<med_id>", methods=["DELETE"])
|
||||
def api_deleteMedication(med_id):
|
||||
@@ -73,7 +99,12 @@ def register(app):
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
pass
|
||||
existing = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid})
|
||||
if not existing:
|
||||
return flask.jsonify({"error": "not found"}), 404
|
||||
postgres.delete("med_logs", {"medication_id": med_id})
|
||||
postgres.delete("medications", {"id": med_id, "user_uuid": user_uuid})
|
||||
return flask.jsonify({"deleted": True}), 200
|
||||
|
||||
# ── Medication Logging (take / skip / snooze) ─────────────────
|
||||
|
||||
@@ -83,8 +114,20 @@ def register(app):
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
med = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid})
|
||||
if not med:
|
||||
return flask.jsonify({"error": "not found"}), 404
|
||||
data = flask.request.get_json() or {}
|
||||
pass
|
||||
log_entry = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"medication_id": med_id,
|
||||
"user_uuid": user_uuid,
|
||||
"action": "taken",
|
||||
"scheduled_time": data.get("scheduled_time"),
|
||||
"notes": data.get("notes"),
|
||||
}
|
||||
log = postgres.insert("med_logs", log_entry)
|
||||
return flask.jsonify(log), 201
|
||||
|
||||
@app.route("/api/medications/<med_id>/skip", methods=["POST"])
|
||||
def api_skipMedication(med_id):
|
||||
@@ -92,8 +135,20 @@ def register(app):
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
med = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid})
|
||||
if not med:
|
||||
return flask.jsonify({"error": "not found"}), 404
|
||||
data = flask.request.get_json() or {}
|
||||
pass
|
||||
log_entry = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"medication_id": med_id,
|
||||
"user_uuid": user_uuid,
|
||||
"action": "skipped",
|
||||
"scheduled_time": data.get("scheduled_time"),
|
||||
"notes": data.get("reason"),
|
||||
}
|
||||
log = postgres.insert("med_logs", log_entry)
|
||||
return flask.jsonify(log), 201
|
||||
|
||||
@app.route("/api/medications/<med_id>/snooze", methods=["POST"])
|
||||
def api_snoozeMedication(med_id):
|
||||
@@ -101,8 +156,12 @@ def register(app):
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
data = flask.request.get_json()
|
||||
pass
|
||||
med = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid})
|
||||
if not med:
|
||||
return flask.jsonify({"error": "not found"}), 404
|
||||
data = flask.request.get_json() or {}
|
||||
minutes = data.get("minutes", 15)
|
||||
return flask.jsonify({"snoozed_until_minutes": minutes}), 200
|
||||
|
||||
# ── Medication Log / History ──────────────────────────────────
|
||||
|
||||
@@ -112,7 +171,17 @@ def register(app):
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
pass
|
||||
med = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid})
|
||||
if not med:
|
||||
return flask.jsonify({"error": "not found"}), 404
|
||||
days = flask.request.args.get("days", 30, type=int)
|
||||
logs = postgres.select(
|
||||
"med_logs",
|
||||
where={"medication_id": med_id},
|
||||
order_by="created_at DESC",
|
||||
limit=days * 10,
|
||||
)
|
||||
return flask.jsonify(logs), 200
|
||||
|
||||
@app.route("/api/medications/today", methods=["GET"])
|
||||
def api_todaysMeds():
|
||||
@@ -120,7 +189,26 @@ def register(app):
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
pass
|
||||
meds = postgres.select("medications", where={"user_uuid": user_uuid, "active": True})
|
||||
from datetime import datetime
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
result = []
|
||||
for med in meds:
|
||||
times = med.get("times", [])
|
||||
taken_times = [
|
||||
log["scheduled_time"]
|
||||
for log in postgres.select(
|
||||
"med_logs",
|
||||
where={"medication_id": med["id"], "action": "taken"},
|
||||
)
|
||||
if log.get("scheduled_time", "").startswith(today)
|
||||
]
|
||||
result.append({
|
||||
"medication": med,
|
||||
"scheduled_times": times,
|
||||
"taken_times": taken_times,
|
||||
})
|
||||
return flask.jsonify(result), 200
|
||||
|
||||
# ── Adherence Stats ───────────────────────────────────────────
|
||||
|
||||
@@ -130,7 +218,27 @@ def register(app):
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
pass
|
||||
days = flask.request.args.get("days", 30, type=int)
|
||||
meds = postgres.select("medications", where={"user_uuid": user_uuid, "active": True})
|
||||
result = []
|
||||
for med in meds:
|
||||
logs = postgres.select(
|
||||
"med_logs",
|
||||
where={"medication_id": med["id"]},
|
||||
limit=days * 10,
|
||||
)
|
||||
taken = sum(1 for log in logs if log.get("action") == "taken")
|
||||
skipped = sum(1 for log in logs if log.get("action") == "skipped")
|
||||
total = taken + skipped
|
||||
adherence = (taken / total * 100) if total > 0 else 0
|
||||
result.append({
|
||||
"medication_id": med["id"],
|
||||
"name": med["name"],
|
||||
"taken": taken,
|
||||
"skipped": skipped,
|
||||
"adherence_percent": round(adherence, 1),
|
||||
})
|
||||
return flask.jsonify(result), 200
|
||||
|
||||
@app.route("/api/medications/<med_id>/adherence", methods=["GET"])
|
||||
def api_medAdherence(med_id):
|
||||
@@ -138,7 +246,26 @@ def register(app):
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
pass
|
||||
med = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid})
|
||||
if not med:
|
||||
return flask.jsonify({"error": "not found"}), 404
|
||||
days = flask.request.args.get("days", 30, type=int)
|
||||
logs = postgres.select(
|
||||
"med_logs",
|
||||
where={"medication_id": med_id},
|
||||
limit=days * 10,
|
||||
)
|
||||
taken = sum(1 for log in logs if log.get("action") == "taken")
|
||||
skipped = sum(1 for log in logs if log.get("action") == "skipped")
|
||||
total = taken + skipped
|
||||
adherence = (taken / total * 100) if total > 0 else 0
|
||||
return flask.jsonify({
|
||||
"medication_id": med_id,
|
||||
"name": med["name"],
|
||||
"taken": taken,
|
||||
"skipped": skipped,
|
||||
"adherence_percent": round(adherence, 1),
|
||||
}), 200
|
||||
|
||||
# ── Refills ───────────────────────────────────────────────────
|
||||
|
||||
@@ -148,8 +275,18 @@ def register(app):
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
med = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid})
|
||||
if not med:
|
||||
return flask.jsonify({"error": "not found"}), 404
|
||||
data = flask.request.get_json()
|
||||
pass
|
||||
if not data:
|
||||
return flask.jsonify({"error": "missing body"}), 400
|
||||
allowed = ["quantity_remaining", "refill_date", "pharmacy_notes"]
|
||||
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("medications", updates, {"id": med_id, "user_uuid": user_uuid})
|
||||
return flask.jsonify(result[0] if result else {}), 200
|
||||
|
||||
@app.route("/api/medications/refills-due", methods=["GET"])
|
||||
def api_refillsDue():
|
||||
@@ -157,4 +294,19 @@ def register(app):
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
pass
|
||||
days_ahead = flask.request.args.get("days_ahead", 7, type=int)
|
||||
from datetime import datetime, timedelta
|
||||
cutoff = (datetime.now() + timedelta(days=days_ahead)).strftime("%Y-%m-%d")
|
||||
meds = postgres.select(
|
||||
"medications",
|
||||
where={"user_uuid": user_uuid},
|
||||
)
|
||||
due = []
|
||||
for med in meds:
|
||||
qty = med.get("quantity_remaining")
|
||||
refill_date = med.get("refill_date")
|
||||
if qty is not None and qty <= 7:
|
||||
due.append(med)
|
||||
elif refill_date and refill_date <= cutoff:
|
||||
due.append(med)
|
||||
return flask.jsonify(due), 200
|
||||
|
||||
149
api/routes/routine_sessions_extended.py
Normal file
149
api/routes/routine_sessions_extended.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
Routine Sessions Extended API - pause, resume, abort, notes, duration tracking
|
||||
"""
|
||||
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
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/sessions/<session_id>/pause", methods=["POST"])
|
||||
def api_pauseSession(session_id):
|
||||
"""Pause an active 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": "session not found"}), 404
|
||||
if session.get("status") != "active":
|
||||
return flask.jsonify({"error": "session not active"}), 400
|
||||
result = postgres.update(
|
||||
"routine_sessions",
|
||||
{"status": "paused", "paused_at": datetime.now().isoformat()},
|
||||
{"id": session_id}
|
||||
)
|
||||
return flask.jsonify({"status": "paused"}), 200
|
||||
|
||||
@app.route("/api/sessions/<session_id>/resume", methods=["POST"])
|
||||
def api_resumeSession(session_id):
|
||||
"""Resume a paused 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": "session not found"}), 404
|
||||
if session.get("status") != "paused":
|
||||
return flask.jsonify({"error": "session not paused"}), 400
|
||||
result = postgres.update(
|
||||
"routine_sessions",
|
||||
{"status": "active", "paused_at": None},
|
||||
{"id": session_id}
|
||||
)
|
||||
return flask.jsonify({"status": "active"}), 200
|
||||
|
||||
@app.route("/api/sessions/<session_id>/abort", methods=["POST"])
|
||||
def api_abortSession(session_id):
|
||||
"""Abort a session with reason. Body: {reason: string}"""
|
||||
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": "session not found"}), 404
|
||||
data = flask.request.get_json() or {}
|
||||
reason = data.get("reason", "Aborted by user")
|
||||
result = postgres.update(
|
||||
"routine_sessions",
|
||||
{"status": "aborted", "abort_reason": reason, "completed_at": datetime.now().isoformat()},
|
||||
{"id": session_id}
|
||||
)
|
||||
return flask.jsonify({"status": "aborted", "reason": reason}), 200
|
||||
|
||||
@app.route("/api/sessions/<session_id>/note", methods=["POST"])
|
||||
def api_addSessionNote(session_id):
|
||||
"""Add a note to the session. Body: {step_index: int, note: string}"""
|
||||
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": "session not found"}), 404
|
||||
data = flask.request.get_json()
|
||||
if not data or not data.get("note"):
|
||||
return flask.jsonify({"error": "missing note"}), 400
|
||||
note_entry = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"session_id": session_id,
|
||||
"step_index": data.get("step_index"),
|
||||
"note": data["note"],
|
||||
}
|
||||
note = postgres.insert("routine_session_notes", note_entry)
|
||||
return flask.jsonify(note), 201
|
||||
|
||||
@app.route("/api/sessions/<session_id>/duration", methods=["PUT"])
|
||||
def api_setSessionDuration(session_id):
|
||||
"""Record actual duration. Body: {actual_duration_minutes: int}"""
|
||||
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": "session not found"}), 404
|
||||
data = flask.request.get_json()
|
||||
if not data:
|
||||
return flask.jsonify({"error": "missing body"}), 400
|
||||
duration = data.get("actual_duration_minutes")
|
||||
if duration is None:
|
||||
return flask.jsonify({"error": "missing actual_duration_minutes"}), 400
|
||||
result = postgres.update(
|
||||
"routine_sessions",
|
||||
{"actual_duration_minutes": duration},
|
||||
{"id": session_id}
|
||||
)
|
||||
return flask.jsonify(result[0] if result else {}), 200
|
||||
|
||||
@app.route("/api/sessions/<session_id>", methods=["GET"])
|
||||
def api_getSessionDetails(session_id):
|
||||
"""Get session details with all notes."""
|
||||
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": "session not found"}), 404
|
||||
notes = postgres.select("routine_session_notes", {"session_id": session_id}, order_by="created_at")
|
||||
routine = postgres.select_one("routines", {"id": session["routine_id"]})
|
||||
steps = postgres.select("routine_steps", {"routine_id": session["routine_id"]}, order_by="position")
|
||||
return flask.jsonify({
|
||||
"session": session,
|
||||
"routine": routine,
|
||||
"steps": steps,
|
||||
"notes": notes,
|
||||
}), 200
|
||||
154
api/routes/routine_stats.py
Normal file
154
api/routes/routine_stats.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""
|
||||
Routine Stats API - completion statistics and streaks
|
||||
"""
|
||||
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
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/routines/<routine_id>/stats", methods=["GET"])
|
||||
def api_routineStats(routine_id):
|
||||
"""Get completion stats for a routine. Query: ?days=30"""
|
||||
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": "routine not found"}), 404
|
||||
days = flask.request.args.get("days", 30, type=int)
|
||||
sessions = postgres.select(
|
||||
"routine_sessions",
|
||||
where={"routine_id": routine_id},
|
||||
limit=days,
|
||||
)
|
||||
completed = sum(1 for s in sessions if s.get("status") == "completed")
|
||||
aborted = sum(1 for s in sessions if s.get("status") == "aborted")
|
||||
total_duration = sum(
|
||||
s.get("actual_duration_minutes", 0) or 0
|
||||
for s in sessions
|
||||
if s.get("actual_duration_minutes")
|
||||
)
|
||||
avg_duration = total_duration / completed if completed > 0 else 0
|
||||
completion_rate = (completed / len(sessions) * 100) if sessions else 0
|
||||
return flask.jsonify({
|
||||
"routine_id": routine_id,
|
||||
"routine_name": routine["name"],
|
||||
"period_days": days,
|
||||
"total_sessions": len(sessions),
|
||||
"completed": completed,
|
||||
"aborted": aborted,
|
||||
"completion_rate_percent": round(completion_rate, 1),
|
||||
"avg_duration_minutes": round(avg_duration, 1),
|
||||
"total_time_minutes": total_duration,
|
||||
}), 200
|
||||
|
||||
@app.route("/api/routines/streaks", methods=["GET"])
|
||||
def api_userStreaks():
|
||||
"""Get all streaks for the user."""
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
streaks = postgres.select("routine_streaks", where={"user_uuid": user_uuid})
|
||||
routines = postgres.select("routines", where={"user_uuid": user_uuid})
|
||||
routine_map = {r["id"]: r["name"] for r in routines}
|
||||
result = []
|
||||
for streak in streaks:
|
||||
result.append({
|
||||
"routine_id": streak["routine_id"],
|
||||
"routine_name": routine_map.get(streak["routine_id"], "Unknown"),
|
||||
"current_streak": streak["current_streak"],
|
||||
"longest_streak": streak["longest_streak"],
|
||||
"last_completed_date": streak.get("last_completed_date"),
|
||||
})
|
||||
return flask.jsonify(result), 200
|
||||
|
||||
@app.route("/api/routines/<routine_id>/streak", methods=["GET"])
|
||||
def api_routineStreak(routine_id):
|
||||
"""Get streak for a specific 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": "routine not found"}), 404
|
||||
streak = postgres.select_one("routine_streaks", {"routine_id": routine_id, "user_uuid": user_uuid})
|
||||
if not streak:
|
||||
return flask.jsonify({
|
||||
"routine_id": routine_id,
|
||||
"routine_name": routine["name"],
|
||||
"current_streak": 0,
|
||||
"longest_streak": 0,
|
||||
"last_completed_date": None,
|
||||
}), 200
|
||||
return flask.jsonify({
|
||||
"routine_id": routine_id,
|
||||
"routine_name": routine["name"],
|
||||
"current_streak": streak["current_streak"],
|
||||
"longest_streak": streak["longest_streak"],
|
||||
"last_completed_date": streak.get("last_completed_date"),
|
||||
}), 200
|
||||
|
||||
@app.route("/api/routines/weekly-summary", methods=["GET"])
|
||||
def api_weeklySummary():
|
||||
"""Get weekly progress summary."""
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
routines = postgres.select("routines", where={"user_uuid": user_uuid})
|
||||
routine_ids = [r["id"] for r in routines]
|
||||
if not routine_ids:
|
||||
return flask.jsonify({
|
||||
"total_completed": 0,
|
||||
"total_time_minutes": 0,
|
||||
"routines_started": 0,
|
||||
"routines": [],
|
||||
}), 200
|
||||
week_ago = (datetime.now() - timedelta(days=7)).isoformat()
|
||||
sessions = postgres.select("routine_sessions", where={"user_uuid": user_uuid})
|
||||
week_sessions = [s for s in sessions if s.get("created_at") and str(s["created_at"]) >= week_ago]
|
||||
completed = [s for s in week_sessions if s.get("status") == "completed"]
|
||||
total_time = sum(
|
||||
s.get("actual_duration_minutes", 0) or 0
|
||||
for s in completed
|
||||
if s.get("actual_duration_minutes")
|
||||
)
|
||||
routine_summaries = []
|
||||
for routine in routines:
|
||||
r_sessions = [s for s in week_sessions if s.get("routine_id") == routine["id"]]
|
||||
r_completed = sum(1 for s in r_sessions if s.get("status") == "completed")
|
||||
routine_summaries.append({
|
||||
"routine_id": routine["id"],
|
||||
"name": routine["name"],
|
||||
"completed_this_week": r_completed,
|
||||
})
|
||||
return flask.jsonify({
|
||||
"total_completed": len(completed),
|
||||
"total_time_minutes": total_time,
|
||||
"routines_started": len(set(s.get("routine_id") for s in week_sessions)),
|
||||
"routines": routine_summaries,
|
||||
}), 200
|
||||
94
api/routes/routine_steps_extended.py
Normal file
94
api/routes/routine_steps_extended.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
Routine Steps Extended API - instructions, step types, and media for steps
|
||||
"""
|
||||
|
||||
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/routines/<routine_id>/steps/<step_id>/instructions", methods=["PUT"])
|
||||
def api_updateStepInstructions(routine_id, step_id):
|
||||
"""Update step instructions. Body: {instructions: string}"""
|
||||
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": "routine not found"}), 404
|
||||
step = postgres.select_one("routine_steps", {"id": step_id, "routine_id": routine_id})
|
||||
if not step:
|
||||
return flask.jsonify({"error": "step not found"}), 404
|
||||
data = flask.request.get_json()
|
||||
if not data:
|
||||
return flask.jsonify({"error": "missing body"}), 400
|
||||
instructions = data.get("instructions")
|
||||
if instructions is None:
|
||||
return flask.jsonify({"error": "missing instructions"}), 400
|
||||
result = postgres.update("routine_steps", {"instructions": instructions}, {"id": step_id})
|
||||
return flask.jsonify(result[0] if result else {}), 200
|
||||
|
||||
@app.route("/api/routines/<routine_id>/steps/<step_id>/type", methods=["PUT"])
|
||||
def api_updateStepType(routine_id, step_id):
|
||||
"""Update step type. Body: {step_type: 'generic'|'timer'|'checklist'|'meditation'|'exercise'}"""
|
||||
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": "routine not found"}), 404
|
||||
step = postgres.select_one("routine_steps", {"id": step_id, "routine_id": routine_id})
|
||||
if not step:
|
||||
return flask.jsonify({"error": "step not found"}), 404
|
||||
data = flask.request.get_json()
|
||||
if not data:
|
||||
return flask.jsonify({"error": "missing body"}), 400
|
||||
step_type = data.get("step_type")
|
||||
allowed_types = ["generic", "timer", "checklist", "meditation", "exercise"]
|
||||
if step_type not in allowed_types:
|
||||
return flask.jsonify({"error": f"invalid step_type. allowed: {allowed_types}"}), 400
|
||||
result = postgres.update("routine_steps", {"step_type": step_type}, {"id": step_id})
|
||||
return flask.jsonify(result[0] if result else {}), 200
|
||||
|
||||
@app.route("/api/routines/<routine_id>/steps/<step_id>/media", methods=["PUT"])
|
||||
def api_updateStepMedia(routine_id, step_id):
|
||||
"""Update step media URL. Body: {media_url: string}"""
|
||||
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": "routine not found"}), 404
|
||||
step = postgres.select_one("routine_steps", {"id": step_id, "routine_id": routine_id})
|
||||
if not step:
|
||||
return flask.jsonify({"error": "step not found"}), 404
|
||||
data = flask.request.get_json()
|
||||
if not data:
|
||||
return flask.jsonify({"error": "missing body"}), 400
|
||||
media_url = data.get("media_url")
|
||||
result = postgres.update("routine_steps", {"media_url": media_url}, {"id": step_id})
|
||||
return flask.jsonify(result[0] if result else {}), 200
|
||||
125
api/routes/routine_tags.py
Normal file
125
api/routes/routine_tags.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""
|
||||
Routine Tags API - structured categories for routines
|
||||
"""
|
||||
|
||||
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/tags", methods=["GET"])
|
||||
def api_listTags():
|
||||
"""List all available tags."""
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
tags = postgres.select("routine_tags", order_by="name")
|
||||
return flask.jsonify(tags), 200
|
||||
|
||||
@app.route("/api/tags", methods=["POST"])
|
||||
def api_createTag():
|
||||
"""Create a new tag. Body: {name, color?}"""
|
||||
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("name"):
|
||||
return flask.jsonify({"error": "missing required field: name"}), 400
|
||||
existing = postgres.select_one("routine_tags", {"name": data["name"]})
|
||||
if existing:
|
||||
return flask.jsonify({"error": "tag already exists"}), 409
|
||||
tag = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"name": data["name"],
|
||||
"color": data.get("color", "#888888"),
|
||||
}
|
||||
result = postgres.insert("routine_tags", tag)
|
||||
return flask.jsonify(result), 201
|
||||
|
||||
@app.route("/api/tags/<tag_id>", methods=["DELETE"])
|
||||
def api_deleteTag(tag_id):
|
||||
"""Delete a tag."""
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
tag = postgres.select_one("routine_tags", {"id": tag_id})
|
||||
if not tag:
|
||||
return flask.jsonify({"error": "tag not found"}), 404
|
||||
postgres.delete("routine_routine_tags", {"tag_id": tag_id})
|
||||
postgres.delete("routine_tags", {"id": tag_id})
|
||||
return flask.jsonify({"deleted": True}), 200
|
||||
|
||||
@app.route("/api/routines/<routine_id>/tags", methods=["POST"])
|
||||
def api_addTagToRoutine(routine_id):
|
||||
"""Add tags to a routine. Body: {tag_ids: [uuid, ...]}"""
|
||||
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": "routine not found"}), 404
|
||||
data = flask.request.get_json()
|
||||
if not data or not data.get("tag_ids"):
|
||||
return flask.jsonify({"error": "missing tag_ids"}), 400
|
||||
for tag_id in data["tag_ids"]:
|
||||
existing_link = postgres.select_one("routine_routine_tags", {"routine_id": routine_id, "tag_id": tag_id})
|
||||
if not existing_link:
|
||||
postgres.insert("routine_routine_tags", {"routine_id": routine_id, "tag_id": tag_id})
|
||||
tags = postgres.execute("""
|
||||
SELECT t.* FROM routine_tags t
|
||||
JOIN routine_routine_tags rt ON t.id = rt.tag_id
|
||||
WHERE rt.routine_id = %s
|
||||
""", {"routine_id": routine_id})
|
||||
return flask.jsonify(tags), 200
|
||||
|
||||
@app.route("/api/routines/<routine_id>/tags/<tag_id>", methods=["DELETE"])
|
||||
def api_removeTagFromRoutine(routine_id, tag_id):
|
||||
"""Remove a tag 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": "routine not found"}), 404
|
||||
postgres.delete("routine_routine_tags", {"routine_id": routine_id, "tag_id": tag_id})
|
||||
return flask.jsonify({"removed": True}), 200
|
||||
|
||||
@app.route("/api/routines/<routine_id>/tags", methods=["GET"])
|
||||
def api_getRoutineTags(routine_id):
|
||||
"""Get tags 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": "routine not found"}), 404
|
||||
tags = postgres.execute("""
|
||||
SELECT t.* FROM routine_tags t
|
||||
JOIN routine_routine_tags rt ON t.id = rt.tag_id
|
||||
WHERE rt.routine_id = %s
|
||||
""", {"routine_id": routine_id})
|
||||
return flask.jsonify(tags), 200
|
||||
168
api/routes/routine_templates.py
Normal file
168
api/routes/routine_templates.py
Normal file
@@ -0,0 +1,168 @@
|
||||
"""
|
||||
Routine Templates API - pre-built routines that users can clone
|
||||
"""
|
||||
|
||||
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/templates", methods=["GET"])
|
||||
def api_listTemplates():
|
||||
"""List all available templates."""
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
templates = postgres.select("routine_templates", order_by="name")
|
||||
for template in templates:
|
||||
steps = postgres.select("routine_template_steps", {"template_id": template["id"]}, order_by="position")
|
||||
template["step_count"] = len(steps)
|
||||
return flask.jsonify(templates), 200
|
||||
|
||||
@app.route("/api/templates", methods=["POST"])
|
||||
def api_createTemplate():
|
||||
"""Create a new template (admin only in production). 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["created_by_admin"] = False
|
||||
template = postgres.insert("routine_templates", data)
|
||||
return flask.jsonify(template), 201
|
||||
|
||||
@app.route("/api/templates/<template_id>", methods=["GET"])
|
||||
def api_getTemplate(template_id):
|
||||
"""Get a template with its steps."""
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
template = postgres.select_one("routine_templates", {"id": template_id})
|
||||
if not template:
|
||||
return flask.jsonify({"error": "template not found"}), 404
|
||||
steps = postgres.select("routine_template_steps", {"template_id": template_id}, order_by="position")
|
||||
return flask.jsonify({"template": template, "steps": steps}), 200
|
||||
|
||||
@app.route("/api/templates/<template_id>/clone", methods=["POST"])
|
||||
def api_cloneTemplate(template_id):
|
||||
"""Clone a template to user's routines."""
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
template = postgres.select_one("routine_templates", {"id": template_id})
|
||||
if not template:
|
||||
return flask.jsonify({"error": "template not found"}), 404
|
||||
template_steps = postgres.select("routine_template_steps", {"template_id": template_id}, order_by="position")
|
||||
new_routine = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"user_uuid": user_uuid,
|
||||
"name": template["name"],
|
||||
"description": template.get("description"),
|
||||
"icon": template.get("icon"),
|
||||
}
|
||||
routine = postgres.insert("routines", new_routine)
|
||||
for step in template_steps:
|
||||
new_step = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"routine_id": routine["id"],
|
||||
"name": step["name"],
|
||||
"instructions": step.get("instructions"),
|
||||
"step_type": step.get("step_type", "generic"),
|
||||
"duration_minutes": step.get("duration_minutes"),
|
||||
"media_url": step.get("media_url"),
|
||||
"position": step["position"],
|
||||
}
|
||||
postgres.insert("routine_steps", new_step)
|
||||
return flask.jsonify(routine), 201
|
||||
|
||||
@app.route("/api/templates/<template_id>", methods=["PUT"])
|
||||
def api_updateTemplate(template_id):
|
||||
"""Update a template."""
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
template = postgres.select_one("routine_templates", {"id": template_id})
|
||||
if not template:
|
||||
return flask.jsonify({"error": "template not found"}), 404
|
||||
data = flask.request.get_json()
|
||||
if not data:
|
||||
return flask.jsonify({"error": "missing body"}), 400
|
||||
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("routine_templates", updates, {"id": template_id})
|
||||
return flask.jsonify(result[0] if result else {}), 200
|
||||
|
||||
@app.route("/api/templates/<template_id>", methods=["DELETE"])
|
||||
def api_deleteTemplate(template_id):
|
||||
"""Delete a template."""
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
template = postgres.select_one("routine_templates", {"id": template_id})
|
||||
if not template:
|
||||
return flask.jsonify({"error": "template not found"}), 404
|
||||
postgres.delete("routine_template_steps", {"template_id": template_id})
|
||||
postgres.delete("routine_templates", {"id": template_id})
|
||||
return flask.jsonify({"deleted": True}), 200
|
||||
|
||||
@app.route("/api/templates/<template_id>/steps", methods=["POST"])
|
||||
def api_addTemplateStep(template_id):
|
||||
"""Add a step to a template. Body: {name, instructions?, step_type?, duration_minutes?, position?}"""
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
template = postgres.select_one("routine_templates", {"id": template_id})
|
||||
if not template:
|
||||
return flask.jsonify({"error": "template not found"}), 404
|
||||
data = flask.request.get_json()
|
||||
if not data or not data.get("name"):
|
||||
return flask.jsonify({"error": "missing required field: name"}), 400
|
||||
max_pos = postgres.select(
|
||||
"routine_template_steps",
|
||||
{"template_id": template_id},
|
||||
order_by="position DESC",
|
||||
limit=1,
|
||||
)
|
||||
next_pos = (max_pos[0]["position"] + 1) if max_pos else 1
|
||||
step = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"template_id": template_id,
|
||||
"name": data["name"],
|
||||
"instructions": data.get("instructions"),
|
||||
"step_type": data.get("step_type", "generic"),
|
||||
"duration_minutes": data.get("duration_minutes"),
|
||||
"media_url": data.get("media_url"),
|
||||
"position": data.get("position", next_pos),
|
||||
}
|
||||
result = postgres.insert("routine_template_steps", step)
|
||||
return flask.jsonify(result), 201
|
||||
@@ -5,6 +5,7 @@ Routines have ordered steps. Users start sessions to walk through them.
|
||||
"""
|
||||
|
||||
import os
|
||||
import uuid
|
||||
import flask
|
||||
import jwt
|
||||
import core.auth as auth
|
||||
@@ -41,7 +42,8 @@ def register(app):
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
pass
|
||||
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():
|
||||
@@ -50,7 +52,14 @@ def register(app):
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
data = flask.request.get_json()
|
||||
pass
|
||||
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/<routine_id>", methods=["GET"])
|
||||
def api_getRoutine(routine_id):
|
||||
@@ -58,7 +67,15 @@ def register(app):
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
pass
|
||||
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/<routine_id>", methods=["PUT"])
|
||||
def api_updateRoutine(routine_id):
|
||||
@@ -67,7 +84,17 @@ def register(app):
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
data = flask.request.get_json()
|
||||
pass
|
||||
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/<routine_id>", methods=["DELETE"])
|
||||
def api_deleteRoutine(routine_id):
|
||||
@@ -75,7 +102,14 @@ def register(app):
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
pass
|
||||
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 ────────────────────────────────────────────────
|
||||
|
||||
@@ -85,7 +119,15 @@ def register(app):
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
pass
|
||||
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/<routine_id>/steps", methods=["POST"])
|
||||
def api_addStep(routine_id):
|
||||
@@ -93,8 +135,30 @@ def register(app):
|
||||
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()
|
||||
pass
|
||||
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/<routine_id>/steps/<step_id>", methods=["PUT"])
|
||||
def api_updateStep(routine_id, step_id):
|
||||
@@ -102,8 +166,21 @@ def register(app):
|
||||
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()
|
||||
pass
|
||||
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/<routine_id>/steps/<step_id>", methods=["DELETE"])
|
||||
def api_deleteStep(routine_id, step_id):
|
||||
@@ -111,7 +188,14 @@ def register(app):
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
pass
|
||||
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/<routine_id>/steps/reorder", methods=["PUT"])
|
||||
def api_reorderSteps(routine_id):
|
||||
@@ -119,8 +203,21 @@ def register(app):
|
||||
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()
|
||||
pass
|
||||
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) ─────────────────────
|
||||
|
||||
@@ -130,7 +227,28 @@ def register(app):
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
pass
|
||||
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():
|
||||
@@ -138,7 +256,17 @@ def register(app):
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
pass
|
||||
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/<session_id>/complete-step", methods=["POST"])
|
||||
def api_completeStep(session_id):
|
||||
@@ -146,8 +274,23 @@ def register(app):
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
data = flask.request.get_json()
|
||||
pass
|
||||
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/<session_id>/skip-step", methods=["POST"])
|
||||
def api_skipStep(session_id):
|
||||
@@ -155,8 +298,22 @@ def register(app):
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
data = flask.request.get_json()
|
||||
pass
|
||||
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/<session_id>/cancel", methods=["POST"])
|
||||
def api_cancelSession(session_id):
|
||||
@@ -164,7 +321,11 @@ def register(app):
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
pass
|
||||
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 ───────────────────────────────────
|
||||
|
||||
@@ -174,7 +335,17 @@ def register(app):
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
pass
|
||||
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 ────────────────────────────────────────
|
||||
|
||||
@@ -184,8 +355,26 @@ def register(app):
|
||||
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()
|
||||
pass
|
||||
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/<routine_id>/schedule", methods=["GET"])
|
||||
def api_getRoutineSchedule(routine_id):
|
||||
@@ -193,7 +382,13 @@ def register(app):
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
pass
|
||||
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/<routine_id>/schedule", methods=["DELETE"])
|
||||
def api_deleteRoutineSchedule(routine_id):
|
||||
@@ -201,4 +396,8 @@ def register(app):
|
||||
user_uuid = _auth(flask.request)
|
||||
if not user_uuid:
|
||||
return flask.jsonify({"error": "unauthorized"}), 401
|
||||
pass
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user