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. SQLite and deployed PostgreSQL migrations have been verified; see deployment validation.

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.
  • Use the link icon on your run page to copy a public link. Anyone with that link can view the run, full route and charts without signing in. Public links are separate from friend visibility; disable a link in the run's sharing settings to revoke access. There is no Explore page or public feed.
  • Share links use the address you opened the app with. A localhost link only works on your own computer; sharing with others requires a reachable app address.
  • Routes are not masked or trimmed. Access control, rather than geographic masking, determines who can see the full route, including start and finish.

Exploring running stats

Open Stats for the query editor with syntax highlighting, contextual autocomplete, and live validation from the execution parser. Use Ctrl+Space for suggestions and Ctrl/Cmd+Enter to run. Format normalizes a valid query; invalid drafts retain the displayed results. Guided controls and the language reference remain available below the editor. CodeMirror 5.65.20 and its completion addon are vendored locally under the MIT license; the editor needs no CDN access.

The DSL supports statistical summaries and percentiles, calculated fields, AND/OR filters, run gaps and changes, rolling workload, running streaks, calendar and metric-bin groups, group filters/ranking, cumulative and rolling trends, scatter plots with correlation, and CSV exports. See the complete DSL reference and examples for units and execution semantics.

For your ten biggest gaps between recorded runs:

runs
  during all time
  summarize gap_since_previous_run, distance
  by run
  sort by gap_since_previous_run desc
  take 10
  compare none

Gaps use complete owner-scoped history before filters and link to the previous run. The explorer uses persisted run summaries without loading recording samples. Cards show metric definitions and usable-run coverage. Select a chart point or group row to inspect contributing runs. Save named views in this browser, or bookmark the results URL; relative queries follow the calendar as runs are added.

Bringing in more activities

Use Settings & imports -> Connect Garmin to upload an exported session, then choose Sync now or leave hourly sync enabled. Any account can also upload an original FIT file through Settings & imports. Operators can import local caches with the command below. 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. Email verification/recovery and shared production rate-limit storage remain outstanding. Deployment instructions and sync behavior are in docs/DEPLOYMENT.md. The local server binds only to loopback; production runs behind the Dokku HTTPS proxy.

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. Uploaded web sessions are encrypted in PostgreSQL using TOKEN_ENCRYPTION_KEY. 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.

The web app supports encrypted Garmin session uploads, manual queued sync, hourly scheduled sync, and a Dokku worker. Email verification/recovery remains unimplemented.

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.

Server sync

Under Settings & imports, upload your exported Garmin session JSON to connect. Use Sync now for manual sync; hourly sync is enabled for new connections. Both use a separate persistent worker and import the latest 100 activities privately. Pause hourly sync or disconnect at any time. See deployment notes.

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%