Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion api/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ 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'),
zone_minutes=data.get('zone_minutes')
)

# Handle optional time series data
Expand Down
6 changes: 6 additions & 0 deletions api/body_insight.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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()
Expand Down
58 changes: 36 additions & 22 deletions api/dashboard.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
# 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
from models.user import db, UserProfile
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')

Expand All @@ -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
})
30 changes: 30 additions & 0 deletions api/sessions.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
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

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
Expand All @@ -23,3 +26,30 @@ def get_sessions():
})

return jsonify(sessions), 200



# Get activity, body insight and sleep data details for a single session
@sessions_bp.route('/<int:session_id>/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()

# 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 = {
"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
56 changes: 56 additions & 0 deletions api/sleep_data.py
Original file line number Diff line number Diff line change
@@ -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('/<int:id>', 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
3 changes: 3 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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():
Expand Down
1 change: 1 addition & 0 deletions models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
from .activity_summary import ActivitySummary
from .metadata import ActivityMetadata
from .body_insight import BodyInsight
from .sleep_data import SleepData
7 changes: 7 additions & 0 deletions models/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ 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)
zone_minutes = db.Column(db.Integer, nullable=True)




Expand All @@ -51,6 +55,9 @@ 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,
"zone_minutes": self.zone_minutes,
"time_series": [ts.as_dict() for ts in self.time_series]
}

Expand Down
13 changes: 11 additions & 2 deletions models/body_insight.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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,
Expand All @@ -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
}
22 changes: 22 additions & 0 deletions models/sleep_data.py
Original file line number Diff line number Diff line change
@@ -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
}