Files
mywhoosh2garmin/docs/superpowers/specs/2026-08-15-mywhoosh-garmin-sync-design.md
Bastian Wagner f6da346e18 plans + specs
2026-08-15 09:00:41 +02:00

621 lines
17 KiB
Markdown

# MyWhoosh -> Garmin Sync Service — Design
Date: 2026-08-15
Status: Draft for user review
## 1. Goal
Build a self-hosted Docker service that periodically checks MyWhoosh for new cycling activities for multiple configured users, downloads each new FIT file, rewrites the device metadata to a Garmin Edge 1030 Plus, and imports the activity into Garmin Connect without forwarding it to Strava.
The service is administered through a local-only web interface protected by a single admin password.
## 2. Scope
### In scope for v1
- Multiple independent sync users.
- Local-only admin UI.
- Admin password supplied via environment variable.
- SQLite persistence.
- Encrypted MyWhoosh and Garmin credentials at rest.
- Persistent per-user MyWhoosh and Garmin token stores.
- Periodic background sync at a configurable interval.
- Manual "sync now" actions.
- MyWhoosh activity discovery and FIT download.
- Binary FIT metadata patching to Garmin Edge 1030 Plus.
- FIT CRC validation and repair.
- Garmin Connect import via `import_activity()`.
- No intentional forwarding to Strava.
- Per-user error isolation, status and retry handling.
- Garmin MFA handling through the admin UI when required.
- Activity history and sync-run history.
### Out of scope for v1
- Public Internet exposure.
- Multi-admin or per-user web logins.
- OAuth/OIDC for the admin UI.
- Editing deployment configuration from the web UI.
- Strava integration.
- Mobile app.
- Distributed workers or external queues.
- Automatic CAPTCHA solving or browser automation for MyWhoosh login.
## 3. High-level architecture
The application runs as one Docker container with a persistent `/data` volume.
Components:
1. FastAPI application.
2. Server-rendered Jinja2 admin UI with HTMX for small interactive actions.
3. SQLite database.
4. Scheduler.
5. Sync manager.
6. Per-user MyWhoosh client.
7. FIT rewriter.
8. Per-user Garmin client/uploader.
9. Credential encryption service.
Data flow:
MyWhoosh -> download FIT -> validate FIT -> patch device metadata -> rewrite CRC -> Garmin `import_activity()` -> persist result.
## 4. Deployment configuration
Configuration is supplied through environment variables, for example:
- `ADMIN_PASSWORD`
- `SECRET_KEY`
- `CREDENTIAL_ENCRYPTION_KEY`
- `SYNC_INTERVAL_MINUTES`
- `DATABASE_URL=sqlite:////data/app.db`
- `DATA_DIR=/data`
The web server binds inside the container and is published only to the trusted local network by Docker configuration.
The web UI does not modify these deployment-level settings.
## 5. Admin authentication
The application has one admin login with no username.
- The password is read from `ADMIN_PASSWORD`.
- The password is never persisted in SQLite.
- Successful login establishes a signed session using `SECRET_KEY`.
- Authentication failures reveal no account details.
- Session cookies should be `HttpOnly` and `SameSite=Lax`.
- If TLS is later placed in front of the service, `Secure` should be enabled for the cookie.
Because the service is intended for LAN-only use, v1 does not introduce a separate identity provider.
## 6. User model
Each sync user is independent.
A user contains:
- id
- display name
- enabled flag
- health state
- encrypted MyWhoosh email/password
- encrypted Garmin email/password
- created/updated timestamps
Credentials are encrypted before being written to SQLite. The encryption key comes exclusively from `CREDENTIAL_ENCRYPTION_KEY`.
Stored passwords are never returned to the browser. When editing a user, an empty password field means "keep the existing password".
## 7. Token storage
Authentication/session tokens are separated per user.
Suggested filesystem layout:
```text
/data/
app.db
tokens/
<user-id>/
mywhoosh.json
garmin/
activities/
<user-id>/
```
The Garmin tokenstore mechanism from `python-garminconnect` should be reused rather than reimplemented.
The MyWhoosh token cache stores the access token and, where usable, the refresh token and associated account metadata.
No user may read or reuse another user's tokenstore.
## 8. MyWhoosh authentication
The service should follow the same direct API-login pattern used by `jdelrue/mywhoosh2garmin` rather than automating the MyWhoosh web login page.
The intended flow is:
1. Load cached per-user token.
2. Attempt an authenticated activities request.
3. If accepted, continue.
4. If unauthorized, attempt API login using the configured MyWhoosh credentials and the Android-style login payload used by the reference project.
5. Persist fresh token data.
6. Retry the operation once.
The implementation must not attempt to defeat or automate CAPTCHA/reCAPTCHA challenges.
The MyWhoosh endpoints are not treated as a stable public API. Changes in these endpoints should surface as a clear `action_required`/authentication or integration failure rather than causing uncontrolled retries.
## 9. MyWhoosh activity discovery
For each enabled user, the MyWhoosh client retrieves recent activities using the authenticated bearer token.
Each MyWhoosh activity must have a stable external activity identifier. The pair `(user_id, mywhoosh_activity_id)` is unique in SQLite.
This makes discovery idempotent: the same activity may be returned on every scheduler run but is processed only once unless it previously failed at a retryable stage.
The client is responsible only for:
- authentication,
- listing activities,
- normalizing metadata,
- downloading the original FIT file.
It has no knowledge of Garmin or FIT rewriting.
## 10. FIT rewriting
The FIT rewriter follows the binary-patching approach from the existing working Python implementation rather than fully decoding and re-encoding the activity.
### 10.1 Device identity
Target device:
- manufacturer: Garmin (`1`)
- product: Edge 1030 Plus (`3570`)
- product name: `Edge 1030 Plus`
- serial number: optional; if absent, the original serial field is left unchanged unless a later compatibility requirement proves otherwise
### 10.2 Patched messages
`file_id` fields when present:
- manufacturer
- product
- optional serial number
- product name
`device_info` fields are patched only for the creator device (`device_index == 0`) when a usable device index is present.
Other sensor/device records should remain unchanged so a trainer, HR sensor or power meter does not become an Edge 1030 Plus accidentally.
If the source FIT lacks enough information to identify creator-specific `device_info` safely, `file_id` remains mandatory and `device_info` patching should be conservative rather than rewriting all device messages.
### 10.3 Binary preservation
The rewriter must preserve all bytes not deliberately changed, except FIT CRC fields.
It must support:
- 12-byte and 14-byte FIT headers,
- little- and big-endian definition architectures,
- compressed timestamp records,
- developer fields,
- changing local message definitions.
### 10.4 Validation
Before patching:
- validate `.FIT` signature,
- validate declared length,
- validate header CRC when present,
- validate file CRC.
After patching:
- rewrite header CRC when present,
- rewrite file CRC,
- validate the output again,
- verify expected target metadata is readable.
Invalid FIT input is a non-retryable activity error unless the original file is later replaced/redownloaded.
## 11. Garmin import
The existing Garmin uploader pattern is reused with `python-garminconnect`.
The final activity is sent using `import_activity()` rather than `upload_activity()` because the desired behavior is to import into Garmin Connect without intentional onward synchronization to Strava.
Per user:
1. Reuse Garmin tokenstore where possible.
2. Login/refresh when required.
3. Call `import_activity()` with the converted FIT path.
4. Record the returned Garmin activity/import identifier if available.
5. Treat known duplicate responses as completed `duplicate`, not as fatal failures.
## 12. Garmin MFA
MFA is modeled as an explicit user state.
If Garmin requires MFA and there is no one-time code available:
- the user's health becomes `action_required`,
- that user's Garmin import attempts pause,
- other users continue syncing normally,
- the dashboard shows that MFA is required.
The admin can submit the one-time MFA code through the local UI.
The code:
- is used only for that login attempt,
- is never written to SQLite,
- is never written to logs,
- is discarded immediately after use.
On successful authentication the Garmin tokenstore is persisted and the user returns to normal sync behavior.
## 13. Activity state machine
An activity progresses through durable stages:
- `discovered`
- `downloaded`
- `converted`
- `imported`
- `duplicate`
- `failed`
Persisted activity fields include:
- internal id
- user id
- MyWhoosh activity id
- activity date/time
- activity name
- original FIT path
- converted FIT path
- current status
- Garmin activity/import id when known
- last error
- created/updated timestamps
Completed terminal states are `imported` and `duplicate`.
A failure must retain the latest successfully completed stage so a retry can resume without repeating unnecessary work.
## 14. Sync-run model
Each user sync invocation creates a sync-run record containing:
- id
- user id
- start time
- finish time
- status (`running`, `success`, `partial`, `failed`)
- discovered count
- imported count
- skipped count
- failed count
- summary error when relevant
Detailed application logs remain on stdout; SQLite stores only UI-relevant summaries.
## 15. Scheduler and concurrency
A central scheduler triggers every `SYNC_INTERVAL_MINUTES`.
On each tick:
1. Load enabled users.
2. Schedule one sync job per user.
3. Allow different users to run concurrently.
4. Enforce at most one active sync per user with a per-user lock.
Manual "sync now" uses exactly the same sync pipeline and the same lock.
If a manual request arrives while that user is already syncing, the application should return a clear "already running" result rather than start another run.
The scheduler must not block because one account is slow, broken or waiting for user action.
## 16. Retry policy
Retries depend on failure type.
### Retryable automatically
- transient network errors
- timeouts
- temporary MyWhoosh/Garmin server errors
- expired session/token after one reauthentication attempt
Within one sync run, use at most a small bounded retry (for example one retry). Further retry occurs on the next scheduler tick.
### Action required
- invalid MyWhoosh credentials
- MyWhoosh login/API behavior changed in a way that prevents authentication
- Garmin MFA required
- invalid Garmin credentials
### Non-retryable per activity
- corrupt/invalid FIT file
- unsupported FIT structure that cannot be safely patched
The admin UI can expose an explicit "retry" action for failed activities after the underlying issue is fixed.
## 17. User health state
Each user has a concise operational state:
- `healthy`
- `syncing`
- `degraded`
- `action_required`
- `disabled`
This state is derived from configuration and recent sync/authentication outcomes and is shown prominently on the dashboard.
## 18. Admin UI
### 18.1 Login
Single password field and submit action.
### 18.2 Dashboard
Shows all users with:
- name
- health state
- MyWhoosh connection state
- Garmin connection state
- last sync
- last imported activity
- primary error/action if any
- "sync now"
- "details"
- MFA action when needed
Includes "add account".
### 18.3 User create/edit
Fields:
- display name
- MyWhoosh email
- MyWhoosh password
- Garmin email
- Garmin password
- enabled flag
Actions:
- save
- test connection
Existing passwords are never rendered back to the browser.
### 18.4 User details
Shows:
- current connection and health states
- most recent sync-run summary
- recent activities and status
- latest errors
Actions:
- sync now
- retry failed activity
- enter Garmin MFA when required
### 18.5 System page
Read-only operational information:
- application version
- configured sync interval
- last scheduler tick
- next expected tick
- account count
- activity count
Action:
- sync all now
## 19. UI technology
Use:
- FastAPI
- Jinja2
- HTMX
- small application-specific CSS
Do not introduce Angular, React, Tailwind or Bootstrap for v1 unless requirements change.
HTMX is used for bounded actions such as:
- sync now
- test connection
- submit MFA
- retry activity
The application remains server-rendered and easy to operate as one container.
## 20. Security requirements
- Never log passwords, bearer tokens, session tokens, encryption keys or MFA codes.
- Encrypt stored MyWhoosh and Garmin credentials.
- Keep tokenstores under the persistent data directory with restrictive filesystem permissions where possible.
- Escape all user-visible data rendered into HTML.
- Protect state-changing web requests against CSRF.
- Validate all IDs against the authenticated admin session rather than trusting client-provided paths blindly.
- Use prepared/ORM parameterized database access.
- Do not expose decrypted credentials through API responses or templates.
## 21. Cleanup and retention
For v1, original and converted FIT files are retained because they are valuable for debugging failed imports.
Automated retention/cleanup can be added later after operating behavior is known.
## 22. Error isolation
Failure of one user must never prevent other users from syncing.
Examples:
- User A imports successfully while User B requires Garmin MFA.
- User C may have invalid MyWhoosh credentials without affecting scheduler execution for A or B.
- A corrupt activity file affects only that activity and user.
## 23. Testing strategy
### FIT rewriter tests
- valid 12-byte header FIT
- valid 14-byte header FIT
- invalid header CRC
- invalid file CRC
- malformed/truncated definitions
- developer fields preserved
- compressed timestamp records handled
- Edge 1030 Plus manufacturer/product patched correctly
- non-creator `device_info` unchanged
- output CRC valid
- bytes outside expected metadata and CRC locations unchanged
### MyWhoosh client tests
Use mocked HTTP responses for:
- valid cached token
- expired token followed by successful login
- invalid credentials
- transient server error
- activity listing
- FIT download
Do not make live MyWhoosh requests in the normal unit test suite.
### Garmin uploader tests
Use a fake/protocol-compatible Garmin client for:
- tokenstore login
- successful import
- duplicate
- MFA required
- invalid login
- transient import failure
### Sync manager tests
- new activity full happy path
- discovered activity is not duplicated
- resume from downloaded
- resume from converted
- retry after transient Garmin failure
- one user's failure does not affect another
- per-user lock prevents concurrent duplicate sync
### Web tests
- admin login success/failure
- unauthenticated routes redirect/reject
- create/edit/disable user
- password never returned
- manual sync action
- MFA submission lifecycle
- CSRF on mutating requests
## 24. Suggested module boundaries
```text
app/
main.py
config.py
auth/
admin.py
db/
models.py
session.py
repositories.py
security/
credentials.py
mywhoosh/
client.py
models.py
tokenstore.py
fit/
rewriter.py
crc.py
models.py
garmin/
uploader.py
sync/
manager.py
scheduler.py
states.py
web/
routes.py
forms.py
templates/
static/
tests/
```
Each unit should depend on explicit interfaces/protocols where external services are involved so tests do not require live accounts.
## 25. Primary design decisions
1. One local admin instead of user-facing authentication.
2. Multiple independent sync accounts.
3. SQLite for durable application state.
4. Environment variables for deployment secrets/configuration.
5. Encrypted service credentials at rest.
6. Per-user tokenstores.
7. Direct MyWhoosh API login pattern; no CAPTCHA automation.
8. Binary FIT patching rather than decode/re-encode.
9. Garmin Edge 1030 Plus product ID `3570`.
10. Conservative creator-device patching.
11. Garmin `import_activity()` for Garmin-only import behavior.
12. FastAPI + Jinja2 + HTMX for a small single-container admin UI.
13. Parallel sync across users, serialized sync within each user.
14. Durable activity stages for resumable/idempotent sync.
## 26. Acceptance criteria for v1
The system is ready for v1 when:
1. It runs from Docker with persistent `/data` storage.
2. The admin can log in locally using the environment-configured password.
3. The admin can add at least two independent users.
4. Each user can authenticate independently to MyWhoosh and Garmin.
5. New MyWhoosh activities are discovered automatically on schedule.
6. FIT files are downloaded and patched to Garmin Edge 1030 Plus metadata with valid CRCs.
7. Converted activities are imported into the corresponding Garmin Connect account using `import_activity()`.
8. Already processed activities are not imported again.
9. Garmin MFA for one user can be resolved through the UI and does not block other users.
10. A failure for one user or one activity does not stop the scheduler.
11. The dashboard shows current state, recent syncs and actionable errors.
12. Secrets and MFA codes do not appear in logs or browser responses.