From 6470d54a31290f2e7ff7b8d407582566deba42ba Mon Sep 17 00:00:00 2001 From: Yajat Sharma Date: Fri, 25 Sep 2026 03:44:01 +0530 Subject: [PATCH] Fix runs page regression (layout and pagination) --- PROJECT_SUMMARY.md | 1 + app.py | 96 +++++++++++++++++++++++++++++ templates/index.html | 3 +- tests/test_api_runs.py | 133 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 tests/test_api_runs.py diff --git a/PROJECT_SUMMARY.md b/PROJECT_SUMMARY.md index 4f7cce2..d1bb00f 100644 --- a/PROJECT_SUMMARY.md +++ b/PROJECT_SUMMARY.md @@ -116,6 +116,7 @@ RunRush utilizes a monolithic architecture with server-side rendering, enhanced ### Internal API Endpoints (JSON) * `/add` (POST) - Logs a new manual run. * `/edit/`, `/delete/` - Run modifications. +* `/api/runs` (GET) - Returns paginated, filtered, and sorted runs for the progressive dashboard list. * `/api/parse-import` (POST) - Parses uploaded Strava CSVs. * `/api/parse-screenshot` (POST) - Sends image to Google GenAI for OCR run data extraction. * `/api/heatmap-data` (GET) - Returns GeoJSON/Coordinate data for Leaflet maps. diff --git a/app.py b/app.py index a1c74a1..e2183bd 100644 --- a/app.py +++ b/app.py @@ -2102,6 +2102,102 @@ def update_settings(): # ---------- MONTHLY PROGRESS ---------- + +@app.route("/api/runs") +def api_runs(): + if not require_login(): + return jsonify({"error": "Unauthorized"}), 401 + user = get_current_user() + + sort_by = request.args.get("sort", "date") + filter_opt = request.args.get("filter", "all") + try: + offset = int(request.args.get("offset", 0)) + limit = int(request.args.get("limit", 15)) + if offset < 0 or limit < 0: + raise ValueError() + limit = min(limit, 50) # safe upper bound + except ValueError: + return jsonify({"error": "Invalid offset or limit"}), 400 + + conn = get_db() + + base_query = "SELECT * FROM runs WHERE user_id = ?" + + if sort_by == "distance_asc": + order_clause = " ORDER BY distance_km ASC, date DESC" + elif sort_by == "distance" or sort_by == "distance_desc": + order_clause = " ORDER BY distance_km DESC, date DESC" + elif sort_by == "time_asc": + order_clause = " ORDER BY time_min ASC, date DESC" + elif sort_by == "time" or sort_by == "time_desc": + order_clause = " ORDER BY time_min DESC, date DESC" + elif sort_by == "pace_asc": + order_clause = " ORDER BY pace ASC, date DESC" + elif sort_by == "pace" or sort_by == "pace_desc": + order_clause = " ORDER BY pace DESC, date DESC" + elif sort_by == "cal_asc": + order_clause = " ORDER BY calories ASC, date DESC" + elif sort_by == "cal" or sort_by == "cal_desc": + order_clause = " ORDER BY calories DESC, date DESC" + elif sort_by == "date_asc": + order_clause = " ORDER BY date ASC, id ASC" + else: # default = date or date_desc + order_clause = " ORDER BY date DESC, id DESC" + + runs = conn.execute(base_query + order_clause, (user["id"],)).fetchall() + + # Filtering (Must match main route logic) + filtered_runs = list(runs) + today = get_today() + + if filter_opt == "last7": + cutoff = today - timedelta(days=7) + temp = [] + for r in runs: + try: + d = datetime.strptime(str(r["date"])[:10], "%Y-%m-%d").date() + if d >= cutoff: + temp.append(r) + except Exception: + continue + filtered_runs = temp + + elif filter_opt == "month": + current_year = today.year + current_month = today.month + month_runs = [] + for r in runs: + try: + d = datetime.strptime(str(r["date"])[:10], "%Y-%m-%d").date() + if d.year == current_year and d.month == current_month: + month_runs.append(r) + except Exception: + continue + filtered_runs = month_runs + + elif filter_opt == "5k10k": + temp = [] + for r in runs: + dist = r["distance_km"] + if (4.5 <= dist <= 5.5) or (9.0 <= dist <= 11.0): + temp.append(r) + filtered_runs = temp + + conn.close() + + # Pagination + total_count = len(filtered_runs) + paginated = filtered_runs[offset : offset + limit] + + return jsonify({ + "status": "success", + "runs": [dict(r) for r in paginated], + "total": total_count, + "offset": offset, + "limit": limit + }) + @app.route("/api/monthly-progress", methods=["GET"]) def api_monthly_progress(): if not require_login(): diff --git a/templates/index.html b/templates/index.html index 880adcc..5f7b85e 100644 --- a/templates/index.html +++ b/templates/index.html @@ -3636,7 +3636,7 @@

+
@@ -3656,6 +3656,7 @@

30 runs can eventually retrieve every run.""" + with app.app_context(): + from app import get_db + conn = get_db() + hashed_pin = User.hash_pin('1234') + conn.execute("INSERT INTO users (username, pin) VALUES ('userF', ?)", (hashed_pin,)) + user_f = conn.execute("SELECT id FROM users WHERE username='userF'").fetchone()['id'] + for i in range(100): + conn.execute("INSERT INTO runs (user_id, distance_km, time_min, pace, calories, date) VALUES (?, 5, 30, 6, 300, '2026-09-01')", (user_f,)) + conn.commit() + + client.post('/login', data={'username': 'userF', 'pin': '1234'}) + + offset = 0 + limit = 15 + total_loaded = 0 + + while True: + resp = client.get(f'/api/runs?offset={offset}&limit={limit}') + batch = resp.json["runs"] + if not batch: + break + total_loaded += len(batch) + offset += limit + + assert total_loaded == 100