feat: build Stride local app with Garmin imports and friends-only sharing
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user