Stride

A multi-user running dashboard built with Flask, Jinja, SQLAlchemy, Leaflet and Chart.js. The local version supports accounts, activity imports, dashboards, full route maps, charts, and sharing with accepted friends.

Run the app

.\scripts\start-app.ps1

Or run the steps individually:

uv sync --locked
uv run --locked flask --app app db upgrade
uv run --locked python run.py

Open http://127.0.0.1:5000. On first launch, create your account with your name, email, and a new Stride password. No username is required. When exactly one local Garmin account cache exists, setup attaches its downloaded activities to this first account, privately. Setup is only enabled for local requests before an account exists. Subsequent accounts start empty.

The local app uses SQLite in instance/dashboard.db. Set DATABASE_URL to a PostgreSQL connection string to use PostgreSQL. Database changes use Alembic migrations. Local schema creation/upgrade has been verified on SQLite; PostgreSQL runtime validation remains pending because Docker was unavailable locally.

Accounts and sharing

  • Sign up with name, email and password; sign in with email and password.
  • A generated internal profile identifier replaces a user-chosen username.
  • Activity visibility is Only me or Accepted friends. Imports default to Only me.
  • Add a friend by their registered email under Friends. The recipient must accept the in-app request. No invitation email is sent by this local version.
  • Accepted friends see each other's shared activities and full recorded routes.
  • Removing a friend immediately revokes access to shared profiles, pages and JSON.
  • There is no Explore page, public feed, or anonymous access to activity data.
  • Routes are not masked or trimmed. Access control, rather than geographic masking, determines who can see the full route, including start and finish.

Bringing in more activities

Run the local sync below, then choose Settings & imports -> Load new downloads. This button imports the cache assigned to your account, without contacting Garmin. Any account can also upload an original FIT file through Settings & imports. Original files remain private and are not exposed by web download routes.

If multiple Garmin caches existed at setup, an operator can explicitly assign one:

uv run --locked flask --app app import-local --email owner@example.com --account-key ACCOUNT_KEY

The application uses CSRF protection, hashed passwords, login throttling, and server-side authorization on every activity page and data endpoint. Before public deployment, finish email verification/recovery, shared production rate-limit storage, encrypted token uploads, scheduled jobs and PostgreSQL/Dokku validation. The current server binds only to loopback and runs without debug mode.

Users will upload FIT files or connect Garmin by authenticating locally with python-garminconnect and uploading their exported session tokens. The server will import activities and provide profiles, charts, maps, and publishing controls.

See IMPLEMENTATION_PLAN.md for the architecture, phased checklist, acceptance criteria, and deployment plan.

Run the local sync

Requires uv. Python 3.12 and dependencies are selected by the project configuration and lockfile.

uv sync --locked
uv run --locked python -m garmin_sync login
uv run --locked python -m garmin_sync sync --limit 5

Run login yourself in an interactive terminal. It prompts for your Garmin email, hidden password, and any MFA code. Never paste those into chat or command-line flags. The login goes directly to Garmin; no web service is running or receiving your password. On Windows, scripts/connect-garmin.ps1 combines login and a five-activity sync.

After login, commands resume the saved session without your password:

uv run --locked python -m garmin_sync verify
uv run --locked python -m garmin_sync sync --limit 20 --type running

These are completed activities, not planned training workouts. The default includes all activity types so the first test can find your most recent recording. The limit is how many recent matching activities to examine, including already downloaded ones; it is not the number of new downloads to find.

Files and sessions

  • Session: instance/garmin/default/garmin_tokens.json (private, ignored by Git).
  • Activities: data/garmin/<account-key>/<activity-id>/ (private, ignored by Git).
  • original.zip or original.fit: unchanged original download.
  • activity.json: activity-list metadata returned by Garmin.
  • activity-N.fit: FIT members, assigned safe generated filenames.
  • activity-N.json: decoded sessions, laps, record samples, counts, and units.
  • manifest.json: completion marker and file checksums for safe retries.
  • last-sync.json: summary of the last completed sync command.

Decoded FIT values retain their reported units (including GPS semicircles), listed in the JSON units map. Datetimes are serialized as UTC. These outputs contain full routes and private raw data. The web app only serves activity data after ownership or accepted-friend authorization.

To load an exported token file from another machine:

uv run --locked python -m garmin_sync --profile imported import-tokens 'C:\path\garmin_tokens.json'
uv run --locked python -m garmin_sync --profile imported sync --limit 5

Exports must use the pinned garminconnect==0.3.17 session format. Old garth oauth1_token.json/oauth2_token.json files are unsupported. The importer validates the session with Garmin before saving it. Use --domain garmin.cn before the subcommand for a China account, consistently for login/import and subsequent syncs.

Named profiles isolate local token files; provider account identity partitions downloads. The same account should have only one actively used session copy: avoid concurrently syncing a copied export on multiple machines/profiles.

Tokens refresh when supported by Garmin and are saved after sync. Run login again when a session expires or is rejected. Tokens grant ongoing account access; they are not app-specific, read-only permissions. This tool only invokes read operations, but possession of its token file provides broader account access.

Local credential storage is plain JSON with restricted permissions on POSIX; on Windows it relies on your directory ACLs. Use a private Windows user directory or restrict this workspace's ACLs on a shared machine. Encrypted database credential storage belongs to the future web implementation, not this local proof of concept. No credentials or real activity files should be committed to Git.

Verification

uv run --locked pytest -q
uv run --locked ruff check garmin_sync tests
uv run --locked ruff format --check garmin_sync tests

The offline tests generate a synthetic FIT file; they never authenticate to Garmin. A live verification requires running login, syncing at least one activity, then running sync again to confirm it skips the previously downloaded files. Record the actual outcome in docs/SYNC_VALIDATION.md.

Current scope: local Flask app and sync CLI. Scheduled worker, token-upload web endpoint, email verification/recovery, and Dokku deployment remain to be implemented.

For an isolated browser test with synthetic data, run uv run python scripts/preview-fixture.py and visit port 5001. This test process uses an in-memory database and auto-login, never reads Garmin files, and must not be deployed. The regular app does not have auto-login or seeded test accounts.

S
Description
No description provided
Readme
1.2 MiB
Languages
Python 70.7%
HTML 16.1%
JavaScript 11.3%
CSS 1.6%
Dockerfile 0.1%