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
+35 -35
View File
@@ -1,15 +1,18 @@
# Multi-user running dashboard: implementation plan
Status: planning only; application not yet implemented.
Date: 2026-09-30
Status: live local sync verified; first Flask app implemented with accounts, imports, charts, maps and friends-only sharing. See docs/APP_PROGRESS.md.
Updated: 2026-10-01
## Product and agreed decisions
Build a self-hosted running platform with user accounts, public profiles,
activity pages, and eventually social features similar to Strava. Initial device
Build Stride, a self-hosted running platform with user accounts, friends-only
profiles, activity pages, and social features similar to Strava. Initial device
is a Garmin Forerunner 265, but the data model must support other sources.
- Python, Flask, Jinja templates, PostgreSQL, SQLAlchemy and Flask-Migrate.
- Name/email/password signup; automatically generated internal profile identifiers.
- Sharing requires an accepted friend relationship. No Explore or public activity pages.
- Friends receive full recorded routes, with no start/end masking.
- Leaflet for routes; Chart.js for graphs; Gunicorn for production HTTP.
- `python-garminconnect` for unofficial Garmin integration; `fitdecode` for FIT parsing.
- Users authenticate to Garmin on their own computer with a helper script and
@@ -26,7 +29,7 @@ is a Garmin Forerunner 265, but the data model must support other sources.
Browser -> Flask web process -> PostgreSQL and private activity file storage.
Local Garmin helper -> user uploads token bundle -> connection validation job.
Background worker -> Garmin -> shared FIT import pipeline -> owned activities.
Public pages -> visibility checks -> sanitized public activity data.
Friends pages -> ownership/friendship checks -> full activity data for authorized viewers.
Start with a PostgreSQL-backed job table to avoid adding Redis for the initial
deployment. The worker claims jobs transactionally, leases them, retries with
@@ -67,11 +70,11 @@ pyproject.toml
## Phase 0: prove the token workflow
- [ ] Select and pin a compatible Python and `python-garminconnect` version for
- [x] Select and pin a compatible Python and `python-garminconnect` version for
both helper and server; lock dependencies. Do not assume old garth exports work.
- [ ] Write a local helper with hidden password input and interactive MFA.
- [x] Write a local helper with hidden password input and interactive MFA.
Authenticate directly with Garmin and export the supported session JSON.
- [ ] Wrap exports with an explicit bundle format and library version, or accept
- [x] Wrap exports with an explicit bundle format and library version, or accept
the pinned library's raw token JSON with a documented compatibility check.
- [ ] Test with the owner's account: local login, export, load in a Linux
container, identify Garmin account, and download one recorded run.
@@ -79,9 +82,9 @@ pyproject.toml
work from that host; record region/network limitations rather than assuming portability.
- [ ] Confirm refreshed credentials are captured and persisted; exercise expired
or revoked sessions and the reconnect state.
- [ ] Define whether the exported session remains in local use. Avoid simultaneous
- [x] Define whether the exported session remains in local use. Avoid simultaneous
helper/server refreshes of a shared credential set.
- [ ] Document that session tokens grant ongoing Garmin account access and are
- [x] Document that session tokens grant ongoing Garmin account access and are
not an app-specific read-only permission grant.
Exit: a documented end-to-end local-to-host import succeeds without sending a
@@ -114,7 +117,6 @@ identity and ownership; recovery and verification work with test email delivery.
- ActivityLap and ActivitySample: splits and timestamped measurements.
- ActivityFile: private storage key, checksum, size, import provenance.
- ImportJob: type, owner/connection, status, attempts, lease, idempotency key.
- PrivacyZone: owner and private geographic masking settings.
- [ ] Implement bounded FIT uploads and parsing through one shared service.
Validate contents, use generated storage paths, and bound resource consumption.
- [ ] Extract GPS, elapsed/timer time, distance, heart rate, cadence, elevation,
@@ -156,29 +158,27 @@ repeated imports do not create duplicates; failed imports do not leave partial a
Exit: two accounts sync without crossing credentials or data; restarts preserve
progress; expired sessions request reconnect; Garmin downtime leaves the site usable.
## Phase 4: public dashboard and privacy
## Phase 4: dashboard and friend access
- [ ] Build responsive profile, activity list, run detail, and personal dashboard pages.
- [ ] Add weekly/monthly mileage, pace, elevation, splits, and heart-rate charts.
- [ ] Use Leaflet routes with a chosen map tile provider, attribution, and usage policy.
- [ ] Add owner editing, publish/unpublish, delete, and configurable auto-publishing.
- [ ] Support private and public visibility first; add followers-only with Phase 5.
- [ ] Apply authorization to HTML, JSON, chart data, downloads, feeds, and caches.
- [ ] Mask private route zones on the server before public serialization; do not
leak original coordinates through chart payloads or connect lines across masked gaps.
- [ ] Make originals downloadable only by their owner initially.
- [ ] Invalidate public caches when visibility or masking settings change.
- [ ] Define aggregates so private runs cannot leak through public totals or feeds.
- [x] Build responsive profile, activity list, run detail, and personal dashboard pages.
- [x] Add weekly mileage, pace, splits, and heart-rate charts.
- [x] Use Leaflet routes with attribution; review tile provider for hosted deployment.
- [x] Add owner title editing and private/friends visibility controls.
- [x] Enforce ownership and friendship on profiles, HTML and JSON routes.
- [x] Show the full route to authorized viewers without geographic masking.
- [x] Exclude private activities from friends' totals and feeds.
- [ ] Add configurable automatic sharing, deletion and owner data export.
Exit: public visitors see only published, sanitized data; another signed-in user
cannot access private activity details or files by guessing IDs.
Exit: anonymous visitors and non-friends cannot see activity data. Accepted friends
see only friends-shared activities, including the full recorded route.
## Phase 5: social MVP
- [ ] Add following/unfollowing with private follow requests where applicable.
- [ ] Add a paginated feed filtered by current visibility and relationship state.
- [ ] Implement followers-only activities and test unfollow/block effects.
- [ ] Add account blocking, reporting, and basic site moderation before broad signup.
- [x] Add requests using an existing account's email and explicit recipient acceptance.
- [x] Add friends-only feed and profiles; remove public Explore functionality.
- [x] Add decline/cancel/remove; removal immediately revokes data access.
- [ ] Paginate the feed beyond its initial recent-50 limit.
- [ ] Add account blocking, reporting, and basic moderation before broad signup.
- [ ] Add kudos and comments after the core feed, with ownership checks and abuse limits.
Deferred: challenges, segments, leaderboards, live tracking, mobile apps, coaching,
@@ -204,11 +204,11 @@ additional wearable integrations, and a separate frontend application.
## Verification strategy
- Unit tests: FIT metric calculation, malformed files, duplicate detection,
privacy masking including repeated zone crossings, and credential serialization.
friendship authorization and full-route preservation, and credential serialization.
- Integration tests against PostgreSQL: per-user authorization, refresh races,
leases/retries, disconnect races, migrations, and transactional imports.
- Browser tests: register -> upload FIT -> inspect private run -> publish ->
public view -> unpublish; local-token connection with mocked Garmin responses.
- Browser tests: register -> upload FIT -> inspect private run -> share with friends ->
accept friendship -> view -> remove friendship -> denied; local-token connection with mocked Garmin responses.
- Use synthetic or explicitly consented and sanitized FIT fixtures. Never commit
real tokens, passwords, home coordinates, or personal raw recordings.
- Keep live Garmin tests opt-in; run the Phase 0 smoke test with the owner's account.
@@ -219,17 +219,17 @@ additional wearable integrations, and a separate frontend application.
1. Token portability proven and integration constraints recorded.
2. Accounts plus manual FIT upload and private activity view.
3. Public profiles, publishing controls, and server-side route privacy.
3. Friends-only profiles, sharing controls and full-route authorized views.
4. Per-user token upload, automatic sync, and reconnect flow.
5. Following/feed and operational readiness for invited users.
6. Broader signup after privacy, recovery, moderation, and restore checks pass.
## Decisions to resolve during implementation
- Product name and public domain; repository folder is a working name.
- Hosted domain for Stride; repository folder is a working name.
- Initial signup mode, email provider, and map tile provider.
- History backfill limit and file/account quotas.
- Public aggregate semantics and handling of activity updates/deletions upstream.
- Friends-only aggregate semantics and handling of activity updates/deletions upstream.
- Remote Git hosting/provider, visibility, and repository name if remote hosting is desired.
## References