Files
Stride/IMPLEMENTATION_PLAN.md
T

13 KiB
Raw Blame History

Multi-user running dashboard: implementation plan

Status: planning only; application not yet implemented. Date: 2026-09-30

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 is a Garmin Forerunner 265, but the data model must support other sources.

  • Python, Flask, Jinja templates, PostgreSQL, SQLAlchemy and Flask-Migrate.
  • 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. Public pages -> visibility checks -> sanitized public activity data.

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:

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

  • 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. Authenticate directly with Garmin and export the supported session JSON.
  • 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.
  • 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 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.
    • 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, 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: public dashboard and privacy

  • 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.

Exit: public visitors see only published, sanitized data; another signed-in user cannot access private activity details or files by guessing IDs.

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.
  • 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, privacy masking including repeated zone crossings, 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.
  • 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. Public profiles, publishing controls, and server-side route privacy.
  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.
  • 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.
  • Remote Git hosting/provider, visibility, and repository name if remote hosting is desired.

References

The library's authentication format is version-sensitive. Verify these sources against the pinned release during Phase 0 rather than relying on moving master.