Local running-tracking dashboard, inspired by Strava. It reads your activity
files in KMZ (OpenTracks), GPX or TCX format (e.g. Strava exports),
computes your statistics, and displays the route on a map. Drop files into
data/running/ (see Data folders & external connectors below).
Tracking & map
- π List of all your runs (sorted by date, most recent first)
- πΊοΈ Interactive map of the route (OpenStreetMap), colored by speed, with a speed legend
- π Statistics: distance, moving time, average pace, elevation, max speed, heart rate (when present in the file)
- π Personal notes per run (persisted in
coach.db) - β¬οΈ Drag and drop import: drop KMZ/GPX/TCX activities, nutrition exports or workout files into the Import view. Each zone only accepts the formats its importer understands, and a file is parsed before it replaces anything.
- π§© Exercise-level sessions: record what you actually did (leg press, hip flexor stretchβ¦). Muscle groups are inferred from the name, and a hard session in the 48 h before a pain report is surfaced as a possible link.
- π Shoe automation: pick a default pair and have new runs assigned to it automatically.
- π©Ή Injury watch: episodes derived from the pain already reported in each run's check-in β nothing to record twice β with the base rate, so a link is shown against chance rather than in isolation.
- π§ Load change: last 7 days against the 28 before them, described as ramping down / steady / ramping up / sharp increase. Not a risk score, and withheld after a break.
- π‘οΈ Conditions and RPE divergence: record heat and humidity, and see the runs that felt much harder than the pace implies.
- π§Ύ Report export: a one-page Markdown or print/PDF summary for a coach or physiotherapist, generated locally.
- π€ A coach that can look things up: eleven read-only tools (runs, sessions, load series, weights, nutrition, shoes, plans, injuries, period comparison) so follow-up questions get answers from your records, not from a summary. Bounded and aggregate-only: no coordinates, no full notes.
- π Race-day pacing plan: import the course GPX and get per-kilometre splits at constant effort β climbs slower, descents faster β solved so the total lands on your target time.
- π Critical speed and fade: a speed-duration fit from your own efforts (no reference table, no heart rate) and the second-half slowdown of your long runs, grade-adjusted.
- β°οΈ Grade handling: GAP uses the Minetti (2002) energy-cost polynomial with the gradient measured over at least 50 m, so GPS altitude noise cannot invent a climb.
- π Dark theme: follows the operating system, one click to override; the choice is remembered.
- π Automatic re-read of the
data/folder (β» button or reload)
Advanced analysis (per run)
- π’ Splits: pace per kilometer
- π Laps: analysis of the manual segments (your interval workouts)
- π¨ Pace zones: time/distance spent in 6 zones (recovery β sprint)
- β°οΈ GAP: grade-adjusted pace (flat-ground equivalent)
Global views
- π₯ Personal heatmap: all your routes overlaid
- π Records: best efforts (400 m, 1 km, 1 mile β marathon) + repeated routes (matched runs) to track your progression
- π Monthly statistics: distance/time/elevation per month + comparison
- π― Goals: weekly / monthly / yearly, in distance, time or elevation, with a progress bar
Coaching (pace-based, no heart rate needed)
- π Training load: daily pace-based load (zone-weighted minutes), or session-RPE Γ duration when you log an effort (Foster scale)
- ποΈ Fitness / fatigue / form (CTL / ATL / TSB): Coggan Performance Management Chart with "fresh / neutral / fatigued" bands
β οΈ Injury risk (ACWR): Acute:Chronic Workload Ratio (Gabbett 2016) with alert bands and trend- ποΈ Intensity distribution (80/10/10): time in easy / moderate / hard vs the polarised ideal (Seiler 2010): 80% easy, 10% moderate, 10% hard
- π« VO2max & race predictor: effective VO2max (Daniels VDOT) + race-time predictions (Riegel), with predicted-vs-actual comparison
- ποΈ Training plans: periodized plan generator (race or weekly-volume goal), 80/20 distribution, +10% weekly progression, deload every 4th week, plus scheduled strength/mobility/rest support sessions
- π©Ή Post-run check-in: RPE, session intent, surface, completion, readiness, sleep, stress, legs, a structured pain log (location/side/severity/timing/ trend) and a race flag β the injury-prevention loop
- π Training planner: a weekly view merging runs from your files with strength, mobility, cross-training and rest sessions (planned/completed/ skipped); support load is tracked separately from running load
- β½ Fuel & recovery (optional): a daily check-in for hydration, energy, appetite, steps, fueling and supplements (adherence only), linked to your weight and training load. Coach insights flag rapid weight loss, low energy and sedentary days β never medical or dietary prescriptions.
- π€ Assistant coach (optional): rule-based insight cards that always work offline, plus a conversational coach powered by a local Ollama model or any OpenAI-compatible endpoint. The LLM only receives compact derived metrics β never your GPS coordinates.
Home & accessibility
- π Showcase home: all KPIs and the AI coach side by side, then schedule, race history and stats β the most important features first.
- βΏ Accessible: WCAG 2.1/2.2 AA-oriented β skip link, landmarks, keyboard navigation, ARIA states, visible focus, reduced-motion support and contrast-safe colors (automated axe-core audit clean on every view).
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt.venv/bin/python run.pyThen open http://127.0.0.1:8000 in your browser.
The app is fully functional offline. The conversational coach is opt-in. Model
metadata lives in models.json (base URL, model name, roles); keys live
in .env (gitignored) or your OS environment. The coach panel has a model
selector, so you can switch between your providers.
- Edit
models.jsonwith your OpenAI-compatible endpoints (apiBase,model,apiKeyEnv). - Put the keys in
.env:ALBERT_API_KEY=... GEMMA_API_KEY=...
- Restart and open Coaching β Coach (or Home).
A local Ollama entry is included (ollama-local, no key needed) as an
offline fallback.
Only derived metrics are sent (load, CTL/ATL/TSB, ACWR, intensity split, VO2max, recent summaries) β never raw GPS trackpoints or coordinates. Keys are resolved server-side and never sent to the browser.
See docs/AI_MODELS.md for fields, chatPath, roles and troubleshooting. Which
model actually gives the better coaching answers is a separate question:
docs/AI_BENCHMARK.md describes the local benchmark that compares them.
Thinking models (e.g.
qwen3.5) spend tokens on hidden reasoning before answering. RaisemaxTokensfor that model or use a non-thinking model.
open-running-coach/
βββ data/ # Your activity files (KMZ / GPX / TCX)
βββ coach.db # SQLite database (notes, goals) β created automatically
βββ .env.example # Optional LLM coach configuration
βββ run.py # Server launcher
βββ requirements.txt
βββ app/
βββ main.py # FastAPI app + API routes
βββ parsers/ # Format parsers (kmz.py, gpx.py, tcx.py, model.py)
βββ connectors/ # External imports (OpenNutriTracker, generic CSV/JSON)
βββ activity.py # Basic statistics (distance, pace, elevation)
βββ analysis.py # Advanced analyses (splits, laps, zones, GAP, recordsβ¦)
βββ coaching.py # Coaching analytics (load, fitness, ACWR, 80/20, VO2max, plans)
βββ coach_llm.py # Optional LLM coach (prompt + provider call)
βββ store.py # SQLite persistence (notes, goals, profile, RPE, feedback, sessions, plans)
βββ config.py # Configuration (pace zones, GAP, coaching modelsβ¦)
βββ static/ # Frontend (HTML/CSS/JS + vendored Leaflet)
| Method | Route | Description |
|---|---|---|
| GET | /api/summary |
Global totals (runs, distance, time, elevation) |
| GET | /api/activities |
List of runs (summary) |
| GET | /api/activities/{id} |
Full detail (trackpoints + markers + analyses) |
| PUT | /api/activities/{id}/note |
Save a run's note |
| GET | /api/heatmap |
Points of all routes (heatmap) |
| GET | /api/best-efforts |
All-time best efforts over reference distances |
| GET | /api/matched-runs |
Groups of identical routes |
| GET | /api/monthly |
Monthly statistics + comparison |
| GET/POST | /api/goals |
List / create a goal |
| PUT/DELETE | /api/goals/{id} |
Update / delete a goal |
| PUT | /api/activities/{id}/rpe |
Set/clear a run's RPE (1β10) |
| GET | /api/coaching/load |
Daily training-load series |
| GET | /api/coaching/fitness |
CTL / ATL / Form (TSB) series |
| GET | /api/coaching/acwr |
Injury risk (ACWR) series |
| GET | /api/coaching/intensity |
Intensity distribution (80/10/10 target) |
| GET | /api/coaching/vo2max |
Effective VO2max + race predictions |
| GET/POST | /api/coaching/plans |
List / generate a training plan |
| GET/DELETE | /api/coaching/plans/{id} |
Get / delete a training plan |
| GET/PUT | /api/activities/{id}/feedback |
Get / save post-run feedback (+ RPE) |
| GET/POST | /api/sessions |
List / create support sessions |
| PUT/DELETE | /api/sessions/{id} |
Update / delete a support session |
| GET | /api/nutrition |
List fuel & recovery check-ins |
| PUT | /api/nutrition/{date} |
Upsert a daily check-in |
| DELETE | /api/nutrition/{id} |
Delete a check-in |
| GET/POST | /api/supplements |
List / add supplements |
| PUT/DELETE | /api/supplements/{id} |
Update / delete a supplement |
| GET | /api/coaching/insights |
Rule-based insight cards (offline) |
| GET | /api/coaching/chat/status |
Whether the LLM coach is configured |
| POST | /api/coaching/chat |
Ask the coach (session history in the body) |
| POST | /api/coaching/review |
Generate a structured weekly review |
| POST | /api/reload |
Re-read the data/ folder |
The app reads three formats and normalises them all to the same internal model:
- KMZ (OpenTracks) β a ZIP containing
doc.kml; trackpoints, speed, heart rate, cadence, point type, segments and markers. - GPX β universal export (Strava "Download my data" gives one
.gpxper activity). Trackpoints, elevation, time, heart rate/cadence from extensions;<trkseg>maps to segments. Strava's numeric<type>codes are mapped (9 β running, 12 β trail running). - TCX (Garmin) β explicit
<Lap>elements map directly to segments;<Sport>maps to the activity type; heart rate, cadence and speed.
When the same run is present in several files (e.g. OpenTracks KMZ and a Strava GPX), a fingerprint (first/last GPS timestamps + distance) deduplicates them, keeping the richest copy. Formats without a speed array (GPX) fall back to positional speed for moving-time detection.
Heart rate and cadence are often absent; the app only displays them when present. FIT is not yet supported (planned for later).
Data is split by domain:
data/
βββ running/ # KMZ / GPX / TCX (OpenTracks, Strava, Garmin)
βββ workout/ # strength / mobility / cross-training exports
βββ nutrition/ # nutrition exports (daily macros + weight)
Drop files in the matching folder and reload. Imports are idempotent: each file is tracked, so re-importing an unchanged file does nothing.
| Domain | F-Droid option | Notes |
|---|---|---|
| Nutrition | Waistline | On F-Droid (GPL-3). OpenNutriTracker has the cleaner, documented JSON export but is not on F-Droid yet β install it from GitHub Releases (e.g. via Obtainium, which is on F-Droid) or Google Play. |
| Workout | wger | On F-Droid, self-hostable, with a REST API. |
The OpenNutriTracker connector below works with the app regardless of how you install it (GitHub APK, Obtainium or Play Store). Waistline's diary CSV uses
;separators with localised headers, so it needs a dedicated adapter.
Export from Settings β Export / Import App Data β Export (JSON or CSV
bundle) and put the .zip in data/nutrition/. The connector reads
user_tracked_day (daily kcal / protein / carbs / fat) and weight_log
(weight history). It only writes the macros and weights, so manual check-in
fields are preserved.
A generic CSV is also accepted (, or ;, English or French headers, ISO
or DD/MM/YYYY dates, optional unit suffixes):
Date;Calories (kcal);ProtΓ©ines (g);Glucides (g);Lipides (g);Poids (kg)
20/10/2026;2100;120;260;70;72,4That is the Waistline diary-export style (semicolon + French headers + decimal comma), so Waistline works without a per-app adapter.
Export or map any workout app to this CSV (or a JSON list with the same
fields) and put it in data/workout/:
date,kind,title,duration_min,rpe,focus,notes,status
2026-10-21,strength,Upper body,45,6,shoulders|triceps,Imported,completed
2026-10-23,mobility,Hips and calves,20,2,hip|calves,,completedkind: strength Β· mobility Β· cross Β· rest Β· physio Β· otherfocus: pipe-separated body areasstatus: planned Β· completed Β· skipped
A wger-style JSON is also accepted:
{ "workouts": [ { "name": "Upper body", "sessions": [
{ "date": "2026-10-20", "duration_min": 45, "rpe": 6,
"focus": ["shoulders", "triceps"],
"exercises": [ { "name": "Bench press", "sets": 3, "reps": 8, "weight_kg": 60 } ] }
] } ] }exercises are summarized into the session notes (e.g. "Bench press 3x8 @ 60 kg").
Instead of exporting wger by hand, the app can pull an account: Import β
wger over its REST API β Connect to wger. It reads weight, nutrition and
strength sessions once β GET only, nothing written back β and stores them as
ordinary exports in data/, so a second pull replaces the first result.
Mint a key under User settings β API key on your wger instance β either the
permanent token or the 120-day JWT refresh token β and put it in .env:
WGER_TOKEN=your-personal-api-key
# Self-hosted instance only:
# WGER_BASE_URL=https://wger.example.orgThe key stays on the server (never logged, never in the database, never sent to
the browser). From a terminal the same pull is
.venv/bin/python -m app.connectors.wger_api. wger has no GPS tracks, so
running files still come from OpenTracks, Strava or Garmin. Details, skipped
items and error messages: docs/WGER.md.
Sample files are included so you can see the result immediately:
examples/nutrition/waistline-sample.csv and
examples/workout/wger-sample.json β copy them into the matching data/
folder to try them. Sessions appear in the Planner. Re-importing a changed
file replaces the sessions it previously created.
.venv/bin/pip install -r requirements-dev.txt # pytest + ruff
.venv/bin/python -m pytest # test suite
.venv/bin/ruff check . # lint
.venv/bin/ruff format --check . # formattingThe suite covers the coaching models (load, CTL/ATL/TSB, ACWR, intensity, VDOT, Riegel, plans), the analysis layer (zones, GAP, splits, GPS robustness, matched runs), every parser and every connector.
No real activity data is used. All fixtures are generated from
tests/fixtures/:
| Fixture | Reproduces |
|---|---|
make_gpx.py |
Strava export: numeric <type>, no speed sensor, nested HR extension |
make_tcx.py |
Garmin export: one <Lap> per lap, HeartRateBpm, TPX/Speed |
make_kmz.py |
OpenTracks export: <MultiTrack>, parallel arrays, IDLE samples, no HR |
make_nutrition.py |
Waistline CSV (French headers, decimal comma) and JSON: meals, steps, weight |
make_workout.py |
wger JSON and generic CSV: sessions with exercise summaries |
track.py |
One canonical run rendered into all three activity formats |
test_formats.py parses the same run from GPX, TCX and KMZ and asserts that
distance, moving time, pace zones and GAP agree, so a parser cannot drift away
from the shared Activity model.
To try the app on synthetic data in a fresh clone:
.venv/bin/python examples/make_demo_data.pyIt writes 5 activities across the three formats plus nutrition, weight, steps
and workouts into data/. Delete those files when you switch to real data.
CI (.github/workflows/ci.yml) runs three jobs: lint and format, the suite on
Python 3.11β3.14, and a smoke job that generates the demo dataset, boots
uvicorn and probes every API endpoint.
docs/GETTING_STARTED.mdβ quick start for beta testersdocs/USER_MANUAL.mdβ how to use the app (for beta testers)docs/AI_MODELS.mdβ model registry (models.json) and key handlingdocs/AI_BENCHMARK.mdβ comparing models against five prompts and three athlete situationsdocs/DEPLOYMENT.mdβ packaging as a standalone local appdocs/SALES_PROPOSAL_B2C.md/docs/SALES_PROPOSAL_B2B.mdβ product proposalsdocs/research/β cited notes behind every formula (01β09) and the competitive landscape (10)
- Distance: the computation rejects aberrant GPS jumps (implied speed
10 m/s) and falls back to the distance integrated from the recorded speed, which is more reliable. Some activities contain positional noise that can still slightly inflate the distance.
- Elevation & GAP: GPS altitude is noisy; it is smoothed before computing. The GAP relies on a simplified model (linear correction per % of grade) and remains an estimate.
- Pace zones: "standard" bounds (adjustable in
app/config.py,PACE_ZONES) as long as heart-rate data is unavailable. - Matched runs: heuristic detection (grid similarity + distance); it may group two close-but-not-identical routes.
Released under the GNU Affero General Public License v3.0 β see
LICENSE for the full text.
In short: you may use, study, modify and redistribute this software, including commercially, provided that derivative works stay under the same license and that anyone interacting with a modified version over a network is offered its source code.
Third-party components bundled in app/static/vendor/ keep their own
licenses (Leaflet: BSD-2-Clause, Leaflet.heat/simpleheat: MIT) β see
THIRD_PARTY_NOTICES.md.
Copyright (c) 2026 Mathis Certenais.