Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
gpxport
A local web app for visualising Garmin cycling GPX files with AI coaching insights powered by a local LLM.
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
Setup
# 1. Clone and install dependencies
git clone https://github.com/yourname/gpxport
cd gpxport
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:
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