Open-source project
yihong0618/running_page avatar
yihong0618/running_page

running_page: a self-hosted running dashboard built from your own GPS tracks

Make your own running home page

4,536 stars1,429 forksTypeScriptMIT

At a glance

What is it?
yihong0618/running_page turns Strava, Garmin, Nike, Keep and GPX exports into a static running homepage you own. The sync scripts and the Vite front end are the interesting part, and the setup order is the part that bites.
Who is it for?
Adopt running_page if you already export GPS tracks from Strava, Garmin, Nike or Keep and want a static site you control, and if you accept that the data pipeline is a Python toolchain you will occasionally have to repair by hand. Do not adopt it if you need a hosted service with an uptime commitment, or if you want multi-user accounts: the whole design assumes one runner and one repository.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 7 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What running_page actually produces

Most running apps give you a feed and a yearly summary. running_page gives you a repository. You fork it, point a sync script at your account, and the scripts write activity records into a SQLite database at run_page/data.db. A build step then turns that database into a static site: a map of your routes, charts, a GitHub-style contribution heatmap of your training days, and per-activity detail pages. The output is files, not a service, so it can live on Vercel, GitHub Pages or any static host.

The audience is narrow and specific. It is for the runner who already has years of GPS history in Strava, Garmin, Nike Run Club or Keep, and who wants that history rendered on a page with their own domain rather than inside someone else's app. The README's runner table lists pages for users on Strava, Nike, Garmin, Garmin-cn and Keep, which tells you which importers the maintainers expect people to use. If your watch writes plain GPX, TCX or FIT files, the repository has GPX_OUT/, TCX_OUT/, FIT_OUT/ and activities/ directories for exactly that case, so you are not limited to the named services.

How the sync, database and static build fit together

There are two runtimes in one repository, and mixing them up is the most common source of confusion. The Python side lives in run_page/ and does acquisition and analysis: it talks to each provider, normalizes the tracks, and writes rows into SQLite via SQLAlchemy. The TypeScript side lives in src/ and does presentation: Vite builds a React app, react-map-gl and mapbox-gl draw the routes, recharts draws the charts.

The connecting artifact is a JSON file. package.json's data:analysis script runs run_page/gen_svg.py with --from-db --type github and writes assets/github.svg, and the front end reads the generated activity data from src/static/activities.json. That means the site is a pure function of the database plus the build. Nothing on the page queries Strava at runtime, so the page keeps working when a provider changes its API, and it also means the page is only as fresh as your last sync.

That split is also why the project can be automated cheaply. The repository ships .github/workflows/run_data_sync.yml, and the README references setting RUN_TYPE to db_updater inside that file to trigger a one-off migration. Sync on a schedule, commit the regenerated data, let the host rebuild. No server process stays running.

Install and first sync: from fork to a page with your own runs

Start by forking the repository and cloning your fork locally. The Python dependencies are declared in requirements.txt and pyproject.toml, and pyproject.toml sets requires-python to >=3.12, so check your interpreter before installing anything.

bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

The front end uses pnpm, pinned in package.json as [email protected]. Install and start the dev server to confirm the template renders before you add your own data.

bash
corepack enable
pnpm install
pnpm run dev

Now the credentials. Each provider has its own script under run_page/. Garmin is the one with a documented extra step: the README says that since 2023.09.26 Garmin needs a secret_string, generated by a helper script and stored in Actions.

bash
python run_page/get_garmin_secret.py ${email} ${password}
# if cn
python run_page/get_garmin_secret.py ${email} ${password} --is-cn

Run the matching sync script for your provider, for example python3 run_page/garmin_sync.py with the secret string, or python3 run_page/strava_sync.py with client id, client secret and refresh token. Then generate the analysis asset and build.

bash
pnpm run data:analysis
pnpm run build

What you should see: rows in run_page/data.db after the sync, a regenerated assets/github.svg after the analysis step, and a Vite build that renders your activities. The Dockerfile shows the same sequence wired as build arguments (app, nike_refresh_token, secret_string, client_id, client_secret, refresh_token, and for Keep keep_phone_number and keep_password), which is a useful reference for what each importer needs.

The Elevation Gain migration is the upgrade path you will hit

The README is unusually candid about a schema change. An Elevation Gain field was added on 2024.09.29, and any fork made before that date will fail with sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such column: activities.elevation_gain. The fix is a migration script, and if you have no local Python environment the README offers a workaround: set RUN_TYPE to db_updater in .github/workflows/run_data_sync.yml once, let the Action run, then change it back.

bash
python run_page/db_updater.py

Two caveats come with it. Past activities do not gain elevation data from the migration alone; the README says a full reimport is needed to populate it for old runs. And the field itself may be inaccurate, with the README pointing at Strava's Correct Elevation or Garmin's Elev Corrections for better data. The display is also gated: the Elevation Gain column only appears if you modify SHOW_ELEVATION_GAIN in src/utils/const.ts. That is a three-step change (migrate, maybe reimport, toggle a constant) for what looks like one feature, and it is the clearest example of this project's upgrade cost.

Where running_page is the wrong tool

The design assumes one runner per repository. There is no user table, no login, no per-account isolation in the architecture described by the README and the top-level layout. If you want to host dashboards for a running club, you are looking at one fork per member, which is a maintenance burden the project does not try to solve.

It is also not a live tracker. Because the page is built from a database snapshot, a run you finish now will not appear until the next sync and rebuild. If you want real-time splits during a race, this is not that.

Credentials are the other sharp edge. Sync scripts run with tokens or passwords for your Strava, Garmin, Nike or Keep account, and the documented path for Garmin is to generate a secret_string and put it in GitHub Actions. Treat those secrets as production credentials, because they are. The Dockerfile takes them as build arguments, which is convenient for CI and a bad pattern if you build images anywhere shared. And the README explicitly warns against using the maintainer's Mapbox token, pointing at two issues where that came up. Get your own token or stay on MapCN.

Alternatives and what changes if you switch

The obvious alternative is to stay inside Strava, Garmin Connect or Nike Run Club and accept their dashboards. The difference is ownership and shape: those products render your data on their terms, in their layout, with their feature roadmap, and they can change or remove views. running_page gives you the raw SQLite database and a static site you can edit, at the cost of running the pipeline yourself.

A second option is a general-purpose fitness data platform that ingests device files and offers its own analytics. That trade is similar in direction but different in mechanism: you get a maintained product and someone else's schema, and you lose the ability to edit a React component to change how a chart looks. running_page's bet is that the map, the heatmap and the per-run pages are simple enough that a fork is cheaper than a subscription.

A third path is writing your own scripts against the provider APIs and rendering with whatever you like. That is what running_page already did for you, including the less pleasant parts: the Garmin secret_string helper, the Nike refresh token flow, the Keep importer, and the eviltransform and gcoord dependencies that suggest coordinate-system conversion for Chinese map data. Rebuilding that is weeks of work you do not have to repeat.

Licence, maintenance and what you are signing up for

The licence is MIT, declared in both LICENSE and pyproject.toml, and package.json carries the same identifier. Practically, that means you can fork, modify and deploy your own page, including commercially, as long as you keep the copyright notice and licence text. It does not give you any rights to the Strava, Garmin, Nike or Keep APIs you connect to, and nothing here is legal advice: check the terms of each provider before automating against it.

On maintenance, the last push to the repository was on 2026-09-21, two days before this writing, and the repository is not archived, so it is receiving changes. The release history is lumpier: v2.5.1 on 2025-04-21, v2.5 on 2025-04-19, and v2.0 back on 2023-09-23. Tagged releases are not the rhythm of this project; the README's own notes are dated (2023.09.26 for the Garmin secret, 2024.09.29 for Elevation Gain), and those dated notes are what you should read when you upgrade a long-lived fork. A version bump in pyproject.toml is not a signal that the schema changed.

The upgrade cost is real but bounded. Expect to re-run db_updater.py when columns are added, to reimport if you want historical fields backfilled, and to check src/utils/const.ts for display flags that default to off. The dependency lists are long on both sides, so a stale fork will eventually fail to install rather than fail at runtime.

Editorial conclusion

Adopt running_page if you already export GPS tracks from Strava, Garmin, Nike or Keep and want a static site you control, and if you accept that the data pipeline is a Python toolchain you will occasionally have to repair by hand. Do not adopt it if you need a hosted service with an uptime commitment, or if you want multi-user accounts: the whole design assumes one runner and one repository. Before committing, verify three things in your own fork: that your app's sync script runs with your credentials, that the Elevation Gain migration has been applied if you forked before 2024-09-29, and that your map provider is MapCN by default or a Mapbox token you obtained yourself.

Frequently asked questions

What is running_page and who is it for?

It is a project that generates a personal running home page from your own activity data, with sync scripts for Strava, Garmin, Nike and Keep plus GPX, TCX and FIT import paths. It is aimed at a single runner who wants to own the page and the database behind it.

How do I install running_page and run a first sync?

Install the Python dependencies from requirements.txt with Python 3.12 or newer, install the front end with pnpm (pinned as [email protected]), run your provider's script under run_page/, then run pnpm run data:analysis and pnpm run build. Garmin users must first generate a secret_string with run_page/get_garmin_secret.py.

Why does my running_page build fail with no such column: activities.elevation_gain?

Your fork predates the Elevation Gain field added on 2024.09.29. Run python run_page/db_updater.py, or set RUN_TYPE to db_updater in .github/workflows/run_data_sync.yml once and change it back if you have no local environment.

Can I use Mapbox with running_page?

Yes, but you must supply your own Mapbox token. The project now uses MapCN by default, and the README warns against using the maintainer's token.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. yihong0618/running_page on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/yihong0618-running-page.svg)](https://hysenlabs.com/projects/yihong0618-running-page)