import calendar import logging from typing import Optional from fastapi import APIRouter, Depends, Request, Form from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates from app.api.deps import get_training_repository from app.core.database.repository import TrainingRepository from app.core.metrics import ExerciseProgress, summarize from app.core.parsers.obsidian import ObsidianNotesParser, parse_training_data from app.core.templates.templates import BaseTemplateResponse web_router = APIRouter() templates = Jinja2Templates(directory="app/templates") template_renderer = BaseTemplateResponse(templates) def build_progress_chart(progress: ExerciseProgress) -> dict: """Data for the progress chart: two series over time, x is a UTC timestamp in ms.""" if progress.weighted: series = [ ("e1RM, кг", lambda metrics: metrics.e1rm), ("Тоннаж, кг", lambda metrics: metrics.volume or None), ] else: series = [ ("Лучший подход, повторы", lambda metrics: metrics.best_reps), ("Всего повторов за тренировку", lambda metrics: metrics.total_reps), ] chronological = list(reversed(progress.rows)) return { "series": [ { "label": label, "points": [ [calendar.timegm(row.session.date.timetuple()) * 1000, round(value, 1)] for row in chronological if (value := metric(row.metrics)) is not None ], } for label, metric in series ] } @web_router.get("/", response_class=HTMLResponse) async def home( request: Request, repository: TrainingRepository = Depends(get_training_repository), ): """Home page: all trainings from the database in a single table""" return template_renderer.render( request, "index.html", context={"trainings": await repository.list()}, ) @web_router.get("/exercises/", response_class=HTMLResponse) async def exercises( request: Request, name: Optional[str] = None, repository: TrainingRepository = Depends(get_training_repository), ): """Exercise search box and the whole history of the chosen exercise""" query = (name or "").strip() exercise_names = await repository.list_exercise_names() # Exact name, but case-insensitive: what was typed may differ in case names_by_casefold = {item.name.casefold(): item.name for item in exercise_names} selected = names_by_casefold.get(query.casefold()) history = await repository.exercise_history(selected) if selected else [] # Nothing matched exactly: offer the names containing the typed text suggestions = ( [item for item in exercise_names if query.casefold() in item.name.casefold()] if query and not selected else [] ) progress = summarize(history) if history else None return template_renderer.render( request, "exercises.html", context={ "exercise_names": exercise_names, "query": query, "selected": selected, "suggestions": suggestions, "progress": progress, "chart": build_progress_chart(progress) if progress else None, }, ) @web_router.get("/obsidian/list/", response_class=HTMLResponse) async def obsidian_list(request: Request): data = parse_training_data() return template_renderer.render( request=request, template_name="trainings.html", context={"trainings": data} ) @web_router.get("/obsidian/parse", response_class=HTMLResponse) async def obsidian_parse_form(request: Request): return template_renderer.render( request=request, template_name="obsidian/parser.html", context={"trainings": {}} ) @web_router.post("/obsidian/parse", response_class=HTMLResponse) async def obsidian_parse(request: Request, training_data: str = Form(...)): logging.error(training_data) op = ObsidianNotesParser() result = op.parse(data=training_data) logging.error(result) return template_renderer.render( request=request, template_name="trainings.html", context={"trainings": result} )