147 lines
5.1 KiB
Python
147 lines
5.1 KiB
Python
"""
|
|
Victories API - compute noteworthy achievements from session history
|
|
"""
|
|
|
|
import os
|
|
import uuid
|
|
from datetime import datetime, timedelta
|
|
import flask
|
|
import jwt
|
|
import core.auth as auth
|
|
import core.postgres as postgres
|
|
import core.tz as tz
|
|
|
|
|
|
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/victories", methods=["GET"])
|
|
def api_getVictories():
|
|
"""Compute noteworthy achievements. Query: ?days=30"""
|
|
user_uuid = _auth(flask.request)
|
|
if not user_uuid:
|
|
return flask.jsonify({"error": "unauthorized"}), 401
|
|
|
|
days = flask.request.args.get("days", 30, type=int)
|
|
cutoff = tz.user_now() - timedelta(days=days)
|
|
# Convert to naive datetime for comparison with database timestamps
|
|
cutoff = cutoff.replace(tzinfo=None)
|
|
|
|
sessions = postgres.select("routine_sessions", {"user_uuid": user_uuid})
|
|
recent = [
|
|
s for s in sessions if s.get("created_at") and s["created_at"] >= cutoff
|
|
]
|
|
completed = [s for s in recent if s.get("status") == "completed"]
|
|
|
|
victories = []
|
|
|
|
# Comeback: completed after 2+ day gap
|
|
if completed:
|
|
sorted_completed = sorted(completed, key=lambda s: s["created_at"])
|
|
for i in range(1, len(sorted_completed)):
|
|
prev = sorted_completed[i - 1]["created_at"]
|
|
curr = sorted_completed[i]["created_at"]
|
|
gap = (curr - prev).days
|
|
if gap >= 2:
|
|
victories.append(
|
|
{
|
|
"type": "comeback",
|
|
"message": f"Came back after {gap} days — that takes real strength",
|
|
"date": curr.isoformat()
|
|
if hasattr(curr, "isoformat")
|
|
else str(curr),
|
|
}
|
|
)
|
|
|
|
# Weekend completion
|
|
for s in completed:
|
|
created = s["created_at"]
|
|
if (
|
|
hasattr(created, "weekday") and created.weekday() >= 5
|
|
): # Saturday=5, Sunday=6
|
|
victories.append(
|
|
{
|
|
"type": "weekend",
|
|
"message": "Completed a routine on the weekend",
|
|
"date": created.isoformat()
|
|
if hasattr(created, "isoformat")
|
|
else str(created),
|
|
}
|
|
)
|
|
break # Only show once
|
|
|
|
# Variety: 3+ different routines in a week
|
|
routine_ids_by_week = {}
|
|
for s in completed:
|
|
created = s["created_at"]
|
|
if hasattr(created, "isocalendar"):
|
|
week_key = created.isocalendar()[:2]
|
|
if week_key not in routine_ids_by_week:
|
|
routine_ids_by_week[week_key] = set()
|
|
routine_ids_by_week[week_key].add(s.get("routine_id"))
|
|
|
|
for week_key, routine_ids in routine_ids_by_week.items():
|
|
if len(routine_ids) >= 3:
|
|
victories.append(
|
|
{
|
|
"type": "variety",
|
|
"message": f"Completed {len(routine_ids)} different routines in one week",
|
|
"date": None,
|
|
}
|
|
)
|
|
break
|
|
|
|
# Full week consistency: completed every day for 7 consecutive days
|
|
if completed:
|
|
dates_set = set()
|
|
for s in completed:
|
|
created = s["created_at"]
|
|
if hasattr(created, "date"):
|
|
dates_set.add(created.date())
|
|
|
|
sorted_dates = sorted(dates_set)
|
|
max_streak = 1
|
|
current_streak = 1
|
|
for i in range(1, len(sorted_dates)):
|
|
if (sorted_dates[i] - sorted_dates[i - 1]).days == 1:
|
|
current_streak += 1
|
|
max_streak = max(max_streak, current_streak)
|
|
else:
|
|
current_streak = 1
|
|
|
|
if max_streak >= 7:
|
|
victories.append(
|
|
{
|
|
"type": "consistency",
|
|
"message": f"Completed routines every day for {max_streak} days straight",
|
|
"date": None,
|
|
}
|
|
)
|
|
|
|
# Limit and deduplicate
|
|
seen_types = set()
|
|
unique_victories = []
|
|
for v in victories:
|
|
if v["type"] not in seen_types:
|
|
unique_victories.append(v)
|
|
seen_types.add(v["type"])
|
|
|
|
return flask.jsonify(unique_victories[:10]), 200
|