Add indexes and pagination to improve app performance

This commit is contained in:
Peter Stockings
2026-03-09 21:29:16 +11:00
parent acad2def92
commit 7b36a6795d
8 changed files with 264 additions and 208 deletions

View File

@@ -10,6 +10,9 @@ from datetime import date, datetime, timedelta
main = Blueprint('main', __name__)
# Number of readings to show per page in list view
PAGE_SIZE = 50
@main.route('/', methods=['GET'])
def landing():
return redirect(url_for('main.dashboard')) if current_user.is_authenticated else render_template('landing.html')
@@ -29,26 +32,42 @@ def dashboard():
start_date = request.form.get('start_date') or (first_reading and first_reading.strftime('%Y-%m-%d'))
end_date = request.form.get('end_date') or (last_reading and last_reading.strftime('%Y-%m-%d'))
# Fetch filtered readings
readings = fetch_readings(current_user.id, start_date, end_date, user_tz)
# Pagination for list view
page = request.args.get('page', 1, type=int)
# Annotate readings with relative and localized timestamps
annotate_readings(readings, user_tz)
# Fetch paginated readings for the list view
paginated = fetch_readings_paginated(current_user.id, start_date, end_date, user_tz, page, PAGE_SIZE)
# Generate calendar view
week_view = generate_weekly_calendar(readings, datetime.now(user_tz), user_tz)
month_view = generate_monthly_calendar(readings, datetime.now(user_tz), user_tz)
# For calendar/graph/badges, fetch only current month + week readings (much smaller set)
now = datetime.now(user_tz)
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
month_start_utc = month_start.astimezone(utc)
calendar_readings = fetch_readings_for_range(current_user.id, month_start_utc)
# Calculate stats and badges
systolic_avg, diastolic_avg, heart_rate_avg = calculate_weekly_summary(readings)
badges = calculate_progress_badges(readings)
# Annotate all readings with relative and localized timestamps
annotate_readings(paginated.items, user_tz)
annotate_readings(calendar_readings, user_tz)
# Prepare graph data
graph_data = prepare_graph_data(readings)
# Build shared lookup for calendar views (single pass)
readings_by_day = build_readings_by_day(calendar_readings, user_tz)
# Generate calendar views from the shared lookup
week_view = generate_weekly_calendar(readings_by_day, now, user_tz)
month_view = generate_monthly_calendar(readings_by_day, now, user_tz)
# Calculate weekly averages via SQL (much faster than Python)
systolic_avg, diastolic_avg, heart_rate_avg = calculate_weekly_summary_sql(current_user.id)
# Badges from the paginated readings (or full set if needed)
badges = calculate_progress_badges(paginated.items)
# Prepare graph data from calendar readings (current month)
graph_data = prepare_graph_data(calendar_readings)
return render_template(
'dashboard.html',
readings=readings,
readings=paginated.items,
pagination=paginated,
profile=current_user.profile,
badges=badges,
systolic_avg=systolic_avg,
@@ -58,7 +77,7 @@ def dashboard():
start_date=start_date,
end_date=end_date,
month=month_view,
week = week_view,
week=week_view,
date=date,
**graph_data
)
@@ -75,22 +94,32 @@ def get_reading_date_range(user_id, user_tz):
return first, last
def fetch_readings(user_id, start_date, end_date, user_tz):
"""Retrieve readings filtered by date range."""
def fetch_readings_paginated(user_id, start_date, end_date, user_tz, page, per_page):
"""Retrieve paginated readings filtered by date range."""
query = Reading.query.filter_by(user_id=user_id)
if start_date and end_date:
# Convert dates to the user's timezone
start_dt = user_tz.localize(datetime.strptime(start_date, '%Y-%m-%d')).astimezone(utc)
end_dt = user_tz.localize(datetime.strptime(end_date, '%Y-%m-%d')).astimezone(utc) + timedelta(days=1) - timedelta(seconds=1)
query = query.filter(
Reading.timestamp >= start_dt,
Reading.timestamp <= end_dt
)
return query.order_by(Reading.timestamp.desc()).paginate(page=page, per_page=per_page, error_out=False)
def fetch_readings_for_range(user_id, start_utc, end_utc=None):
"""Fetch readings from a UTC start time onwards (for calendar/graph views)."""
query = Reading.query.filter(
Reading.user_id == user_id,
Reading.timestamp >= start_utc
)
if end_utc:
query = query.filter(Reading.timestamp <= end_utc)
return query.order_by(Reading.timestamp.desc()).all()
def annotate_readings(readings, user_tz):
"""Add relative and localized timestamps to readings."""
now = datetime.utcnow()
@@ -98,30 +127,40 @@ def annotate_readings(readings, user_tz):
reading.relative_timestamp = humanize.naturaltime(now - reading.timestamp)
reading.local_timestamp = utc.localize(reading.timestamp).astimezone(user_tz)
def calculate_weekly_summary(readings):
"""Calculate averages for the past week."""
def build_readings_by_day(readings, user_tz):
"""Build a dict mapping dates to readings (single pass, shared by calendar views)."""
readings_by_day = defaultdict(list)
for reading in readings:
local_date = reading.local_timestamp.date() if hasattr(reading, 'local_timestamp') else utc.localize(reading.timestamp).astimezone(user_tz).date()
readings_by_day[local_date].append(reading)
return readings_by_day
def calculate_weekly_summary_sql(user_id):
"""Calculate weekly averages using SQL aggregation (single DB query)."""
one_week_ago = datetime.utcnow() - timedelta(days=7)
weekly_readings = [r for r in readings if r.timestamp >= one_week_ago]
if weekly_readings:
return (
round(sum(r.systolic for r in weekly_readings) / len(weekly_readings), 1),
round(sum(r.diastolic for r in weekly_readings) / len(weekly_readings), 1),
round(sum(r.heart_rate for r in weekly_readings) / len(weekly_readings), 1),
)
result = db.session.query(
func.round(func.avg(Reading.systolic), 1).label('sys_avg'),
func.round(func.avg(Reading.diastolic), 1).label('dia_avg'),
func.round(func.avg(Reading.heart_rate), 1).label('hr_avg'),
).filter(
Reading.user_id == user_id,
Reading.timestamp >= one_week_ago
).first()
if result and result.sys_avg is not None:
return float(result.sys_avg), float(result.dia_avg), float(result.hr_avg)
return 0, 0, 0
def generate_monthly_calendar(readings, selected_date, local_tz):
"""Generate a monthly calendar view."""
def generate_monthly_calendar(readings_by_day, selected_date, local_tz):
"""Generate a monthly calendar view from pre-built readings_by_day."""
today = datetime.now(local_tz).date()
first_day = selected_date.replace(day=1)
start_date = first_day - timedelta(days=(first_day.weekday() + 1) % 7)
end_date = start_date + timedelta(days=41)
readings_by_day = defaultdict(list)
for reading in readings:
local_date = reading.timestamp.astimezone(local_tz).date()
readings_by_day[local_date].append(reading)
return [
{
'day': current_date.day,
@@ -132,23 +171,14 @@ def generate_monthly_calendar(readings, selected_date, local_tz):
for current_date in (start_date + timedelta(days=i) for i in range((end_date - start_date).days + 1))
]
def generate_weekly_calendar(readings, selected_date, local_tz):
"""Generate a weekly calendar view."""
# Get the start of the week (Monday) and the end of the week (Sunday)
def generate_weekly_calendar(readings_by_day, selected_date, local_tz):
"""Generate a weekly calendar view from pre-built readings_by_day."""
today = datetime.now(local_tz).date()
start_of_week = selected_date - timedelta(days=selected_date.weekday())
end_of_week = start_of_week + timedelta(days=6)
# Group readings by day
readings_by_day = defaultdict(list)
for reading in readings:
local_date = reading.timestamp.astimezone(local_tz).date()
readings_by_day[local_date].append(reading)
# Build the weekly calendar view
return [
{
'date': current_date.strftime('%a, %b %d') ,
'date': current_date.strftime('%a, %b %d'),
'is_today': current_date == today,
'readings': readings_by_day.get(current_date.date(), []),
}
@@ -167,22 +197,21 @@ def prepare_graph_data(readings):
def calculate_progress_badges(readings):
"""Generate badges based on user activity and milestones."""
now = datetime.utcnow().date()
streak_count, badges = 1, []
badges = []
if not readings:
return badges
sorted_readings = sorted(readings, key=lambda r: r.timestamp.date())
# Use reversed() instead of re-sorting — readings come in desc order from DB
sorted_readings = list(reversed(readings))
streak_count = 1
daily_streak = True
# Start with the first reading
previous_date = sorted_readings[0].timestamp.date()
for reading in sorted_readings[1:]:
current_date = reading.timestamp.date()
# Check for consecutive daily streaks
if (current_date - previous_date).days == 1:
streak_count += 1
elif (current_date - previous_date).days > 1:
@@ -190,7 +219,6 @@ def calculate_progress_badges(readings):
previous_date = current_date
# Add streak badges
if daily_streak and streak_count >= 1:
badges.append(f"Current Streak: {streak_count} Days")
if daily_streak and streak_count >= 7: