From 1a61310ee69e779e0d24eea593bc5d046fc780ff Mon Sep 17 00:00:00 2001 From: RileyTripcony Date: Tue, 9 Sep 2025 16:17:07 +1000 Subject: [PATCH 1/3] Make body_insight 1:1 relation with activity table. Add error message when trying to create a new bodyinsight if already exists for activity. Expanded sessions endpoint with endpoint to get all activity and bodyinsight data by ID. --- api/body_insight.py | 6 ++++++ api/sessions.py | 22 ++++++++++++++++++++++ models/body_insight.py | 13 +++++++++++-- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/api/body_insight.py b/api/body_insight.py index c592d58..8f271ec 100644 --- a/api/body_insight.py +++ b/api/body_insight.py @@ -27,6 +27,11 @@ def add_body_insight(): if not latest_activity: return jsonify({"error": "No activities found for user"}), 404 activity_id = latest_activity.id + + # Check for existing body_insight for activity + existing = BodyInsight.query.filter_by(activity_id=activity_id).first() + if existing: + return jsonify({"error": "BodyInsight already exists for this activity"}), 409 try: insight = BodyInsight( @@ -49,6 +54,7 @@ def add_body_insight(): altitude_acclimation=data.get('altitude_acclimation'), training_readiness=data.get('training_readiness'), endurance_score=data.get('endurance_score'), + blood_oxygen=data.get('blood_oxygen') ) db.session.add(insight) db.session.commit() diff --git a/api/sessions.py b/api/sessions.py index a2ccc49..36c9d12 100644 --- a/api/sessions.py +++ b/api/sessions.py @@ -1,10 +1,12 @@ from flask import Blueprint, jsonify from models.activity import Activity +from models.body_insight import BodyInsight from models import db from datetime import datetime sessions_bp = Blueprint('sessions_api', __name__) +# Get all sessions summary for table. @sessions_bp.route('', methods=['GET']) def get_sessions(): # replace with authenticated user ID from session/token @@ -23,3 +25,23 @@ def get_sessions(): }) return jsonify(sessions), 200 + + + +# Get activity and body insight details for a single session +@sessions_bp.route('//details', methods=['GET']) +def get_session_details(session_id): + # Fetch the activity + activity = Activity.query.get(session_id) + if not activity: + return jsonify({"error": "Session not found"}), 404 + + # Fetch the linked BodyInsight, if it exists + body_insight = BodyInsight.query.filter_by(activity_id=activity.id).first() + + response = { + "session": activity.as_dict(), + "body_insight": body_insight.as_dict() if body_insight else None + } + + return jsonify(response), 200 \ No newline at end of file diff --git a/models/body_insight.py b/models/body_insight.py index aac932d..bfae0c0 100644 --- a/models/body_insight.py +++ b/models/body_insight.py @@ -4,8 +4,13 @@ class BodyInsight(db.Model): __tablename__ = 'body_insight' id = db.Column(db.Integer, primary_key=True) - activity_id = db.Column(db.Integer, db.ForeignKey('activity.id'), nullable=False) + activity_id = db.Column(db.Integer, db.ForeignKey('activity.id'), nullable=False, unique=True) + activity = db.relationship( + 'Activity', + backref=db.backref('body_insight', uselist=False) # Makes SQLAlchemy treat it as one-to-one + ) + # Performance Metrics vo2_max = db.Column(db.Float, nullable=True) lactate_threshold = db.Column(db.Float, nullable=True) @@ -30,6 +35,8 @@ class BodyInsight(db.Model): training_readiness = db.Column(db.Float, nullable=True) endurance_score = db.Column(db.Float, nullable=True) + # Health Metrics + blood_oxygen = db.Column(db.Float, nullable=True) def as_dict(self): return { "id": self.id, @@ -53,5 +60,7 @@ def as_dict(self): "heat_acclimation": self.heat_acclimation, "altitude_acclimation": self.altitude_acclimation, "training_readiness": self.training_readiness, - "endurance_score": self.endurance_score + "endurance_score": self.endurance_score, + # + "blood_oxygen": self.blood_oxygen } From 392fc34580dada7e27ee909f3c0cf36debda6818 Mon Sep 17 00:00:00 2001 From: RileyTripcony Date: Tue, 9 Sep 2025 18:00:12 +1000 Subject: [PATCH 2/3] full implementation of sessions endpoint returning all data relevent to an activity. --- api/activity.py | 4 ++- api/dashboard.py | 58 +++++++++++++++++++++++++++----------------- api/sessions.py | 14 ++++++++--- api/sleep_data.py | 56 ++++++++++++++++++++++++++++++++++++++++++ app.py | 3 +++ models/__init__.py | 1 + models/activity.py | 4 +++ models/sleep_data.py | 22 +++++++++++++++++ 8 files changed, 136 insertions(+), 26 deletions(-) create mode 100644 api/sleep_data.py create mode 100644 models/sleep_data.py diff --git a/api/activity.py b/api/activity.py index d21b9d6..a354bd1 100644 --- a/api/activity.py +++ b/api/activity.py @@ -28,7 +28,9 @@ def create_activity(): elevation_gain=data['elevation_gain'], elevation_loss=data['elevation_loss'], max_elevation=data['max_elevation'], - min_elevation=data['min_elevation'] + min_elevation=data['min_elevation'], + steps=data.get('steps'), + floors=data.get('floors') ) # Handle optional time series data diff --git a/api/dashboard.py b/api/dashboard.py index e6eda35..f1aec34 100644 --- a/api/dashboard.py +++ b/api/dashboard.py @@ -1,7 +1,3 @@ -# will require edits to the frontend to ensure user data persists: - # src/components/DashboardLanding/DashboardLanding.tsx - # src/components/ProfileAvatar/ProfileAvatar.tsx -# alternatively, find edited files on planner board > 'Add Endpoint for the Dashboard'. import sys from flask import Blueprint, jsonify @@ -9,6 +5,8 @@ from flask_cors import CORS from datetime import datetime, timezone +from models.body_insight import BodyInsight +from models.activity import Activity # Create the Blueprint for dashboard dashboard_bp = Blueprint('dashboard', __name__, url_prefix='/api/dashboard') @@ -20,21 +18,37 @@ def get_dashboard_data(): user_id = 1 # Temporary fixed user user = UserProfile.query.filter_by(id=user_id).first() - if user: - current_utc_time = datetime.now(timezone.utc) - vo2_max = 45 # placeholder for further implementation - - # DEBUG LOGGING - print(f"User fetched: {user.as_dict()}", file=sys.stderr) - - return jsonify({ - 'name': user.name, - 'account': user.account, - 'birthDate': user.birthDate, - 'gender': user.gender, - 'avatar': user.avatar, - 'lastLogin': current_utc_time.isoformat(), - 'vo2Max': vo2_max - }) - - return jsonify({'message': 'User not found'}), 404 + if not user: + return jsonify({'message': 'User not found'}), 404 + + # Find latest activity for this user + latest_activity = ( + Activity.query + .filter_by(user_id=user_id) + .order_by(Activity.begin_time.desc()) + .first() + ) + + # Default vo2_max if no data found + vo2_max = None + + if latest_activity: + body_insight = BodyInsight.query.filter_by(activity_id=latest_activity.id).first() + if body_insight: + vo2_max = body_insight.vo2_max + + current_utc_time = datetime.now(timezone.utc) + + # DEBUG LOGGING + print(f"User fetched: {user.as_dict()}", file=sys.stderr) + print(f"VO2 Max fetched: {vo2_max}", file=sys.stderr) + + return jsonify({ + 'name': user.name, + 'account': user.account, + 'birthDate': user.birthDate, + 'gender': user.gender, + 'avatar': user.avatar, + 'lastLogin': current_utc_time.isoformat(), + 'vo2Max': vo2_max # Can be None if no data found + }) \ No newline at end of file diff --git a/api/sessions.py b/api/sessions.py index 36c9d12..b93e382 100644 --- a/api/sessions.py +++ b/api/sessions.py @@ -1,6 +1,7 @@ from flask import Blueprint, jsonify from models.activity import Activity from models.body_insight import BodyInsight +from models.sleep_data import SleepData from models import db from datetime import datetime @@ -28,7 +29,7 @@ def get_sessions(): -# Get activity and body insight details for a single session +# Get activity, body insight and sleep data details for a single session @sessions_bp.route('//details', methods=['GET']) def get_session_details(session_id): # Fetch the activity @@ -39,9 +40,16 @@ def get_session_details(session_id): # Fetch the linked BodyInsight, if it exists body_insight = BodyInsight.query.filter_by(activity_id=activity.id).first() + # Fetch sleep data for the same date as the activity + sleep_entry = SleepData.query.filter_by( + user_id=activity.user_id, + date=activity.begin_time.date() + ).first() + response = { - "session": activity.as_dict(), - "body_insight": body_insight.as_dict() if body_insight else None + "activity": activity.as_dict(), + "body_insight": body_insight.as_dict() if body_insight else None, + "sleep_data": sleep_entry.as_dict() if sleep_entry else None } return jsonify(response), 200 \ No newline at end of file diff --git a/api/sleep_data.py b/api/sleep_data.py new file mode 100644 index 0000000..3203974 --- /dev/null +++ b/api/sleep_data.py @@ -0,0 +1,56 @@ +# /api/sleep_data.py +from flask import Blueprint, request, jsonify +from models.sleep_data import SleepData +from models import db +from datetime import datetime + +sleep_data_bp = Blueprint('sleep_data', __name__, url_prefix='/api/sleep_data') + +# For now, mimic authenticated user +def get_current_user_id(): + return 1 # Replace with token/session in future + + +# POST new sleep record +@sleep_data_bp.route('', methods=['POST']) +def add_sleep_data(): + data = request.get_json() + user_id = get_current_user_id() + + if not data.get('date') or not data.get('duration_minutes'): + return jsonify({"error": "Missing required fields: date, duration_minutes"}), 400 + + try: + sleep_date = datetime.strptime(data['date'], "%Y-%m-%d").date() + except ValueError: + return jsonify({"error": "Invalid date format. Use YYYY-MM-DD"}), 400 + + sleep_entry = SleepData( + user_id=user_id, + date=sleep_date, + duration_minutes=data['duration_minutes'], + sleep_score=data.get('sleep_score') + ) + + db.session.add(sleep_entry) + db.session.commit() + + return jsonify(sleep_entry.as_dict()), 201 + + +# GET all sleep records for the current user +@sleep_data_bp.route('', methods=['GET']) +def get_all_sleep(): + user_id = get_current_user_id() + entries = SleepData.query.filter_by(user_id=user_id).order_by(SleepData.date.desc()).all() + return jsonify([e.as_dict() for e in entries]), 200 + + +# GET sleep record by ID (scoped to current user) +@sleep_data_bp.route('/', methods=['GET']) +def get_sleep_by_id(id): + user_id = get_current_user_id() + entry = SleepData.query.filter_by(id=id, user_id=user_id).first() + if not entry: + return jsonify({"error": "Sleep record not found for this user"}), 404 + return jsonify(entry.as_dict()), 200 diff --git a/app.py b/app.py index e62cf7e..1250226 100644 --- a/app.py +++ b/app.py @@ -8,6 +8,7 @@ from api.body_insight import body_insight_bp from api.activity import activity_bp from api.sessions import sessions_bp +from api.sleep_data import sleep_data_bp from models import db from dotenv import load_dotenv from api.sync import sync_bp @@ -81,6 +82,8 @@ app.register_blueprint(body_insight_bp, url_prefix='/api/body_insight') app.register_blueprint(activity_bp, url_prefix='/api/activity') app.register_blueprint(sessions_bp, url_prefix='/api/sessions') +app.register_blueprint(sleep_data_bp, url_prefix='/api/sleep_data') + # Main index route (login + welcome) @app.route('/', methods=['GET', 'POST']) def index(): diff --git a/models/__init__.py b/models/__init__.py index b25ac4f..9cc31e6 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -7,3 +7,4 @@ from .activity_summary import ActivitySummary from .metadata import ActivityMetadata from .body_insight import BodyInsight +from .sleep_data import SleepData \ No newline at end of file diff --git a/models/activity.py b/models/activity.py index 0f64fbd..94ee3fc 100644 --- a/models/activity.py +++ b/models/activity.py @@ -25,6 +25,8 @@ class Activity(db.Model): elevation_loss = db.Column(db.Float, nullable=False) max_elevation = db.Column(db.Float, nullable=False) min_elevation = db.Column(db.Float, nullable=False) + steps = db.Column(db.Integer,nullable=True) + floors = db.Column(db.Integer,nullable=True) @@ -51,6 +53,8 @@ def as_dict(self): "elevation_loss": self.elevation_loss, "max_elevation": self.max_elevation, "min_elevation": self.min_elevation, + "steps" : self.steps, + "floors" : self.floors, "time_series": [ts.as_dict() for ts in self.time_series] } diff --git a/models/sleep_data.py b/models/sleep_data.py new file mode 100644 index 0000000..bf1ad53 --- /dev/null +++ b/models/sleep_data.py @@ -0,0 +1,22 @@ +from models import db +from datetime import date + +class SleepData(db.Model): + __tablename__ = 'sleep_data' + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user_profile.id'), nullable=False) + date = db.Column(db.Date, nullable=False) # The night the sleep occurred + duration_minutes = db.Column(db.Integer, nullable=False) # Total sleep in minutes + sleep_score = db.Column(db.Float, nullable=True) # Optional sleep quality score (0-100) + + def as_dict(self): + return { + "id": self.id, + "user_id": self.user_id, + "date": self.date.isoformat(), + "duration_minutes": self.duration_minutes, + "hours": self.duration_minutes // 60, + "minutes": self.duration_minutes % 60, + "sleep_score": self.sleep_score + } From f07ce5b14f11a485ab4930c413acec595f0a7647 Mon Sep 17 00:00:00 2001 From: RileyTripcony Date: Tue, 9 Sep 2025 18:32:04 +1000 Subject: [PATCH 3/3] add zone minutes. this infers that zone minutes is acquired by raw activity data and not calculated on backend. --- api/activity.py | 3 ++- models/activity.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/api/activity.py b/api/activity.py index a354bd1..c670f64 100644 --- a/api/activity.py +++ b/api/activity.py @@ -30,7 +30,8 @@ def create_activity(): max_elevation=data['max_elevation'], min_elevation=data['min_elevation'], steps=data.get('steps'), - floors=data.get('floors') + floors=data.get('floors'), + zone_minutes=data.get('zone_minutes') ) # Handle optional time series data diff --git a/models/activity.py b/models/activity.py index 94ee3fc..4347f5f 100644 --- a/models/activity.py +++ b/models/activity.py @@ -27,6 +27,8 @@ class Activity(db.Model): min_elevation = db.Column(db.Float, nullable=False) steps = db.Column(db.Integer,nullable=True) floors = db.Column(db.Integer,nullable=True) + zone_minutes = db.Column(db.Integer, nullable=True) + @@ -55,6 +57,7 @@ def as_dict(self): "min_elevation": self.min_elevation, "steps" : self.steps, "floors" : self.floors, + "zone_minutes": self.zone_minutes, "time_series": [ts.as_dict() for ts in self.time_series] }