# bikeslop A local web app for visualising Garmin cycling GPX files with AI coaching insights powered by a local LLM. ![bikeslop screenshot](static/bikeslop.png) ## Features - **Route map** — Leaflet.js map with per-segment polylines coloured by heart-rate intensity zone - **Elevation & HR charts** — Chart.js profiles plotted against cumulative distance - **Stats sidebar** — distance, duration, speed, elevation gain, avg/max HR, HR zone breakdown (Karvonen) - **AI coaching insights** — streaming post-ride feedback from a local LLM (via OpenAI-compatible API) - **Garmin sync** — fetch new rides directly from Garmin Connect, including MFA support - **Imperial/metric toggle** — persisted in `localStorage` ## Requirements - Python 3.14+ - [uv](https://github.com/astral-sh/uv) - A local OpenAI-compatible LLM server (e.g. [llama.cpp](https://github.com/ggerganov/llama.cpp), [Ollama](https://ollama.com)) ## Setup ```bash # 1. Clone and install dependencies git clone https://git.torrtle.co/ryan/bikeslop cd bikeslop uv sync # 2. Drop your GPX files into garmin_gpx_exports/ # (or use the in-app "Fetch from Garmin" button) # 3. Start the server LLAMACPP_BASE_URL=http://localhost:8080/v1 uv run uvicorn app:app --reload ``` Open http://localhost:8000 in your browser. ## Configuration | Env var | Default | Description | |---|---|---| | `LLAMACPP_BASE_URL` | `http://localhost:8080/v1` | Base URL of your OpenAI-compatible LLM endpoint | The model name is auto-discovered from `GET /v1/models` at startup. If the endpoint is unreachable, insights generation is still available — only that feature degrades. ## GPX files Place `.gpx` files exported from Garmin Connect (or any GPX-compliant device) in the `garmin_gpx_exports/` directory. The app parses all files at startup. Supported data: - Track points with `lat`/`lon`/`ele`/`time` - Heart rate via the Garmin extension namespace (`ns3:hr`) Activities with no track points (e.g. indoor rides) are listed with an "Indoor" badge and shown without a map or charts. ## Standalone GPX downloader `main.py` is a standalone script for bulk-downloading your Garmin Connect cycling activities as GPX files without running the web server: ```bash uv run python main.py ``` ## API | Method | Path | Description | |---|---|---| | `GET` | `/api/rides` | Summary list of all rides (no track points) | | `GET` | `/api/rides/{id}` | Full ride metadata + cached insight | | `GET` | `/api/rides/{id}/points` | Full lat/lon/ele/hr array | | `POST` | `/api/rides/{id}/insights?rhr=60` | Stream AI coaching insight | | `POST` | `/api/insights/generate-all?rhr=60` | SSE stream — generate insights for all rides | | `POST` | `/api/fetch-rides` | SSE stream — fetch new rides from Garmin Connect | | `POST` | `/api/mfa` | Submit MFA code for an in-progress Garmin session | ## HR zone boundaries (Karvonen) Zones are calculated from your resting HR (configurable in the UI) and the lifetime max HR observed across all rides: | Zone | % of HRR | |---|---| | Z1 | < 50% | | Z2 | 50–60% | | Z3 | 60–70% | | Z4 | 70–80% | | Z5 | > 80% | ## License MIT