ryan c6d9a3b8b6 Add multi-user support: Authelia OIDC, SQLite, Garage S3
- Auth via Authelia OIDC (public client + PKCE) with signed session cookies
- SQLite DB for users, rides, and insights (SQLAlchemy)
- Garage S3 for GPX file storage (boto3)
- All API endpoints scoped to authenticated user
- Dockerfile + docker-compose.yml with Garage service
- Frontend auth check: redirects to /auth/login on 401, shows user badge

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-06 22:12:23 -04:00
2026-07-27 10:07:43 -04:00

bikeslop

A local web app for visualising Garmin cycling GPX files with AI coaching insights powered by a local LLM.

bikeslop screenshot

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://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:

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 5060%
Z3 6070%
Z4 7080%
Z5 > 80%

License

MIT

S
Description
No description provided
Readme 2 MiB
Languages
HTML 52.3%
Python 46.9%
Dockerfile 0.8%