feat: build Stride local app with Garmin imports and friends-only sharing

This commit is contained in:
Peter Stockings
2026-10-01 14:55:59 +10:00
parent 16e71a69d5
commit e37569ad09
45 changed files with 3735 additions and 40 deletions
+151 -5
View File
@@ -1,7 +1,65 @@
# Running dashboard
# Stride
A planned multi-user, self-hosted running dashboard built with Flask and deployed
as one Dokku application.
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 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:
```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, 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
@@ -10,5 +68,93 @@ will import activities and provide profiles, charts, maps, and publishing contro
See [IMPLEMENTATION_PLAN.md](IMPLEMENTATION_PLAN.md) for the architecture,
phased checklist, acceptance criteria, and deployment plan.
Current status: planning only. No application or deployment has been created.
Do not add Garmin credentials, token exports, or personal activity files to Git.
## 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/<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:
```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. 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
```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`.
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.