# 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. Local schema creation/upgrade has been verified on SQLite; PostgreSQL runtime validation is recorded in 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. ### 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: ```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. Before public deployment, finish email verification/recovery, shared production rate-limit storage, and account recovery. Deployment instructions and sync behavior are in docs/DEPLOYMENT.md. 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](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).