Files
Stride/IMPLEMENTATION_PLAN.md

244 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Multi-user running dashboard: implementation plan
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 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
upload exported session tokens. The website does not collect Garmin passwords or MFA codes.
- No dependency on approval for the official Garmin developer program.
- Manual FIT uploads remain available independently of Garmin connectivity.
- One repository, one Docker image, one Dokku application. Web and background
worker are separate processes of that application.
- Original activity files and provider credentials are private. Publishing is
explicitly configured by the activity owner; new accounts default to private imports.
## Architecture
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.
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
backoff, and recovers expired leases. Keep this small; reconsider RQ/Celery and
Redis if queue complexity or throughput warrants it. Do not run a scheduler in
each Gunicorn worker. Schedule due connections from the background worker with
unique job keys and per-connection locks.
Suggested layout:
```text
app/
__init__.py # application factory
config.py
extensions.py
models/
auth/ # signup, sessions, recovery
profiles/
activities/
connections/ # upload, validation, disconnect
admin/ # owner controls and site administration
services/
garmin_adapter.py
credentials.py
fit_import.py
publishing.py
jobs.py
templates/
static/
tools/ # local authentication/export helper
migrations/
tests/
docs/
Dockerfile
Procfile
pyproject.toml
```
## Phase 0: prove the token workflow
- [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.
- [x] Write a local helper with hidden password input and interactive MFA.
Authenticate directly with Garmin and export the supported session JSON.
- [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.
- [ ] Repeat from the intended Dokku host. Confirm that session reuse and refresh
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.
- [x] Define whether the exported session remains in local use. Avoid simultaneous
helper/server refreshes of a shared credential set.
- [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
Garmin password to the website. If it fails, keep FIT uploads usable and record
the integration blocker before building token-based onboarding.
## Phase 1: application foundation and accounts
- [ ] Scaffold Flask application factory, environment configuration, migrations,
structured redacted logging, and health endpoints.
- [ ] Add PostgreSQL local development configuration and dependency lockfile.
- [ ] Implement signup, unique normalized email/username, password hashing,
login/logout, email verification, and single-use expiring password reset links.
- [ ] Use Flask-Login, CSRF protection, secure session cookies, login throttling,
and a configured email delivery provider. Keep credentials out of cookies.
- [ ] Add user time zone, display name, profile slug, and publication defaults.
- [ ] Separate site administrator permissions from activity ownership.
- [ ] Include an option to restrict signup to invitations during the pilot.
Exit: two users can register independently; protected pages and mutations enforce
identity and ownership; recovery and verification work with test email delivery.
## Phase 2: import and activity model
- [ ] Create models and migrations:
- User: identity, authentication, profile, time zone, settings.
- ExternalConnection: owner, provider/account identity, encrypted credential
blob/version, status, sync cursor, last success/error, refresh metadata.
- Activity: owner, source ID, timestamps, sport, title, visibility, summary metrics.
- ActivityLap and ActivitySample: splits and timestamped measurements.
- ActivityFile: private storage key, checksum, size, import provenance.
- ImportJob: type, owner/connection, status, attempts, lease, idempotency key.
- [ ] 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,
laps, and available running metrics. Handle missing fields and paused periods.
- [ ] Keep canonical units and UTC timestamps; render user time zones and units.
- [ ] Deduplicate provider activities by connection/source ID and repeated files
by owner/checksum. Define reconciliation for a manual file later fetched from Garmin.
- [ ] Preserve original files privately and support controlled reprocessing.
- [ ] Handle malformed/unsupported files with actionable import errors.
Exit: a manual FIT upload produces correct summary, route, laps, and charts;
repeated imports do not create duplicates; failed imports do not leave partial activities.
## Phase 3: user-managed Garmin connections and automatic sync
- [ ] Add authenticated token upload with CSRF, strict size/schema/version checks,
and rejection of client-supplied paths or arbitrary provider URLs.
- [ ] Validate the uploaded session against Garmin before enabling sync. Obtain
provider account identity from Garmin rather than trusting uploaded metadata.
- [ ] Bind connections to the current user; enforce one owner per Garmin account
initially and require an explicit replacement flow for changing accounts.
- [ ] Encrypt credentials at application level; supply versioned encryption keys
through Dokku config, separately from database backups; document rotation.
- [ ] Adapt library session loading/saving to per-user encrypted persistence.
If temporary files are necessary, restrict access and remove them reliably.
Never use the library's default shared home-directory token store for all users.
- [ ] Provide connection status, last sync, manual sync, reconnect, and disconnect.
- [ ] Poll connected accounts approximately every 15–30 minutes with jitter,
bounded concurrency, rate-limit backoff, and one active refresh/sync per connection.
- [ ] Persist refreshed credentials atomically and protect against stale writes.
- [ ] Backfill history in bounded jobs with progress and retry support.
- [ ] Periodically revisit recent activities for edits; define upstream deletion
handling explicitly instead of deleting local data based on an absent result.
- [ ] Disconnect cancels pending jobs and deletes stored credentials; prevent
an in-flight worker from restoring credentials or importing after disconnect.
- [ ] Explain that deleting the server's token copy may not revoke other copies.
Do not claim provider-side revocation unless verified for this integration.
Exit: two accounts sync without crossing credentials or data; restarts preserve
progress; expired sessions request reconnect; Garmin downtime leaves the site usable.
## Phase 4: dashboard and friend access
- [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: 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
- [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,
additional wearable integrations, and a separate frontend application.
## Phase 6: Dokku deployment and operations
- [ ] Build one Linux image with web and worker commands; configure a Procfile.
- [ ] Provision PostgreSQL and private persistent storage; deploy one Dokku app
with one web and one worker process initially.
- [ ] Configure HTTPS/proxy handling, domain, email, Flask signing key, encryption
keys, database URL, and storage paths outside source control.
- [ ] Run migrations as a controlled release step; avoid racing worker startup.
- [ ] Add worker heartbeat, import failure visibility, bounded logs, and alerts for
stuck jobs or repeated authentication failures without logging secrets.
- [ ] Back up database and private files; protect encryption keys separately;
rehearse restore and document image rollback/migration compatibility.
- [ ] Implement account deletion covering connections, files, activities, social
data, pending jobs, and documented backup retention.
- [ ] Publish onboarding and operations documentation, including token generation,
upload, reconnect, privacy settings, and manual FIT fallback.
## Verification strategy
- Unit tests: FIT metric calculation, malformed files, duplicate detection,
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 -> 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.
- Deployment smoke test: restart web/worker, retry a failed job, verify persistent
files, and restore a backup in an isolated environment.
## Delivery milestones
1. Token portability proven and integration constraints recorded.
2. Accounts plus manual FIT upload and private activity view.
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
- 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.
- 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
- https://github.com/cyberjunky/python-garminconnect
- https://github.com/cyberjunky/python-garminconnect/blob/master/example.py
- https://github.com/cyberjunky/python-garminconnect/blob/master/garminconnect/client.py
- https://dokku.com/docs/deployment/builders/dockerfiles/
The library's authentication format is version-sensitive. Verify these sources
against the pinned release during Phase 0 rather than relying on moving master.