"""Progress dashboard routes. Displays summary statistics, volume charts, per-exercise progress, and CSV export of workout history. """ import csv import io import json import os import re import tempfile from datetime import date, timedelta from typing import Optional import structlog from fastapi import APIRouter, Depends, File, Query, Request, UploadFile from fastapi.responses import HTMLResponse, StreamingResponse from sqlmodel import Session from app.database import get_db_session from app.models.user import User from app.services.analytics_service import AnalyticsService from app.services.exercise_service import ExerciseService from app.services.export_service import ExportService from app.services.import_service import ImportService from app.services.progression_service import ProgressionService from app.utils.auth import require_active_profile logger = structlog.get_logger(__name__) router = APIRouter(prefix="/dashboard", tags=["dashboard"]) @router.get("", response_class=HTMLResponse) async def dashboard( request: Request, session: Session = Depends(get_db_session), profile: User = Depends(require_active_profile), ): """Render the progress dashboard for the active profile.""" analytics = AnalyticsService(session) stats = analytics.get_user_stats(profile.id) volume_data = analytics.get_volume_by_day(profile.id) personal_records = analytics.get_personal_records(profile.id) adherence = analytics.get_adherence_rate(profile.id) progression_timeline = analytics.get_progression_timeline(profile.id) muscle_recency = analytics.get_muscle_group_recency(profile.id) recent_activity = analytics.get_recent_activity(profile.id) exercise_service = ExerciseService(session) exercises = exercise_service.list_exercises() today = date.today() export_start = (today - timedelta(days=30)).isoformat() export_end = today.isoformat() templates = request.app.state.templates return templates.TemplateResponse("pages/dashboard.html", { "request": request, "stats": stats, "volume_data_json": json.dumps(volume_data), "personal_records": personal_records, "adherence": adherence, "progression_timeline_json": json.dumps(progression_timeline), "muscle_recency": muscle_recency, "recent_activity": recent_activity, "exercises": exercises, "active_profile": profile, "export_start_date": export_start, "export_end_date": export_end, }) @router.get("/export") async def export_csv( request: Request, start_date: Optional[str] = Query(None), end_date: Optional[str] = Query(None), session: Session = Depends(get_db_session), profile: User = Depends(require_active_profile), ): """Export workout history as a CSV file download.""" today = date.today() default_start = today - timedelta(days=30) try: parsed_start = date.fromisoformat(start_date) if start_date else default_start except ValueError: parsed_start = default_start try: parsed_end = date.fromisoformat(end_date) if end_date else today except ValueError: parsed_end = today export_service = ExportService(session) rows = export_service.get_export_rows(profile.id, parsed_start, parsed_end) output = io.StringIO() headers = ["date", "workout_type", "exercise", "set_number", "reps", "weight", "felt_easy"] writer = csv.DictWriter(output, fieldnames=headers) writer.writeheader() writer.writerows(rows) safe_name = re.sub(r"[^a-z0-9_]", "", profile.display_name.lower().replace(" ", "_")) filename = f"sneakyswole_{safe_name}_{parsed_start.isoformat()}_to_{parsed_end.isoformat()}.csv" return StreamingResponse( iter([output.getvalue()]), media_type="text/csv", headers={"Content-Disposition": f'attachment; filename="{filename}"'}, ) @router.post("/import", response_class=HTMLResponse) async def import_db( request: Request, db_file: UploadFile = File(...), session: Session = Depends(get_db_session), profile: User = Depends(require_active_profile), ): """Import workout history from an old SneakySwole database file.""" tmp_fd, tmp_path = tempfile.mkstemp(suffix=".db") try: with os.fdopen(tmp_fd, "wb") as tmp: content = await db_file.read() logger.info("import_upload_received", filename=db_file.filename, size=len(content)) tmp.write(content) import_service = ImportService(session) result = import_service.import_from_db(tmp_path) except Exception: logger.exception("import_failed") raise finally: os.unlink(tmp_path) templates = request.app.state.templates return templates.TemplateResponse("partials/import_results.html", { "request": request, "result": result, }) @router.get("/exercise/{exercise_id}", response_class=HTMLResponse) async def exercise_progress( exercise_id: int, request: Request, session: Session = Depends(get_db_session), profile: User = Depends(require_active_profile), ): """Render per-exercise progress page with charts and suggestions.""" exercise_service = ExerciseService(session) exercise = exercise_service.get_exercise_by_id(exercise_id) analytics = AnalyticsService(session) progress_data = analytics.get_exercise_progress( profile.id, exercise_id, ) progression = ProgressionService(session) suggestion = progression.get_suggestion( profile.id, exercise_id, ) templates = request.app.state.templates return templates.TemplateResponse("pages/exercise_progress.html", { "request": request, "exercise": exercise, "progress_data_json": json.dumps(progress_data), "suggestion": suggestion, "active_profile": profile, })