# 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 ```powershell .\scripts\start-app.ps1 ``` Or run the steps individually: ```powershell 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](docs/DEPLOYMENT.md). ### 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](docs/STATS_DSL.md) for units and execution semantics. For your ten biggest gaps between recorded runs: ```text 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: ```powershell 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](IMPLEMENTATION_PLAN.md) for the architecture, phased checklist, acceptance criteria, and deployment plan. ## Run the local sync Requires [uv](https://docs.astral.sh/uv/). Python 3.12 and dependencies are selected by the project configuration and lockfile. ```powershell 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: ```powershell 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///` (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: ```powershell 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 ```powershell 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](docs/DEPLOYMENT.md).