docs(openspec): propose garmin-import change
Garmin Connect as the third connected-services provider, modeled on wahoo-import but adapted to Garmin's push-first API: OAuth2+PKCE on the existing oauth credential kind, ping/push webhook ingestion with async pg-boss processing and an SSRF allowlist on callback URLs, date-range backfill instead of a pick list (Garmin has no activity-list endpoint), mandatory deregistration handling. Route push (Courses API) explicitly deferred to a follow-up, mirroring wahoo-route-push. Build is fixtures-first; rollout tasks are gated on Garmin Connect Developer Program approval, which runs in parallel. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ebe1b6fd3f
commit
19f1c06d69
5 changed files with 240 additions and 0 deletions
2
openspec/changes/garmin-import/.openspec.yaml
Normal file
2
openspec/changes/garmin-import/.openspec.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
schema: spec-driven
|
||||||
|
created: 2026-06-07
|
||||||
76
openspec/changes/garmin-import/design.md
Normal file
76
openspec/changes/garmin-import/design.md
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The connected-services framework (manifest + capability adapters + `ConnectedServiceManager`) shipped with Wahoo and was stress-tested by Komoot (a `web-login`/`public` provider). Garmin is the third provider and the first whose API is **push-first**: there is no "list my activities" REST endpoint to paginate. Garmin delivers data via webhook notifications — both for new activities and for requested historical backfills — which inverts the import flow the Wahoo UI assumes.
|
||||||
|
|
||||||
|
Garmin specifics that shape everything below (Garmin Connect Developer Program, Activity API):
|
||||||
|
|
||||||
|
- **Auth**: OAuth 2.0 with PKCE (their current scheme; OAuth 1.0a is legacy). Confidential client: token exchange and refresh use `client_id`/`client_secret` *plus* the PKCE verifier on the initial exchange. Access tokens are short-lived (~24 h), refresh tokens long-lived (~3 months).
|
||||||
|
- **Data delivery**: ping/push notifications POSTed to endpoints registered in the **developer portal** (not via API). *Ping* notifications carry a `callbackURL` from which we pull the payload; *push* notifications embed it. FIT files are fetched from a callback URL with the user's token.
|
||||||
|
- **History**: a *backfill* endpoint accepts a time range (chunked, bounded per request) and triggers asynchronous re-delivery of historical activities through the same notification pipeline.
|
||||||
|
- **Deregistration**: when a user revokes access on Garmin's side, Garmin sends a deregistration notification; partner terms require us to act on it.
|
||||||
|
- **Access**: requires an approved developer-program application; evaluation keys are rate-limited; production requires Garmin review.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
- Garmin connect/disconnect from `/settings/connections` with zero framework changes — the manifest/registry seams prove out on a third provider.
|
||||||
|
- New Garmin activities appear in the journal automatically (push pipeline → shared FIT→GPX → activity with stats + PostGIS geometry).
|
||||||
|
- Users can pull in their Garmin history via backfill requests with honest async progress UX.
|
||||||
|
- Garmin-side revocation degrades cleanly (`revoked` status, re-connect prompt, no orphaned polling).
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
- Route push to Garmin devices (Courses API) — a follow-up `RoutePusher` change once import has soaked, like `wahoo-route-push` after `wahoo-import`.
|
||||||
|
- Wellness/health data (Health API): steps, sleep, HR — out of scope; we import *activities* only.
|
||||||
|
- Real-time/live tracking.
|
||||||
|
- Supporting Garmin's legacy OAuth 1.0a.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### Decision: OAuth2 + PKCE rides the existing `oauth` credential kind
|
||||||
|
|
||||||
|
Garmin's token blob is shape-compatible with `OAuthCredentials` (`access_token`, `refresh_token`, `expires_at`), so `credential_kind = 'oauth'` and the existing OAuth `CredentialAdapter` handle storage and refresh. PKCE only affects the **authorize/exchange** steps, which are already per-provider in the manifest's OAuth config — the manifest gains a code-verifier generation + `code_challenge` parameter, with the verifier carried through the OAuth `state` storage (`oauth-state.server.ts`) the same way the existing flow carries its state nonce.
|
||||||
|
|
||||||
|
**Alternative considered:** a new `oauth-pkce` credential kind. Rejected — the *stored* credential is identical; PKCE is a handshake detail, not a credential shape. A new kind would fork the adapter for zero storage benefit.
|
||||||
|
|
||||||
|
### Decision: webhook-first ingestion; ping and push handled by one endpoint
|
||||||
|
|
||||||
|
`/api/sync/webhook/garmin` (generic provider webhook routing) accepts Garmin's notification POSTs. The handler normalizes both delivery styles — *ping* (fetch `callbackURL` for the payload) and *push* (payload inline) — into one internal "activity notification" shape, then: resolve user via `provider_user_id` (Garmin user id captured at connect time), fetch the FIT file, convert via shared `fit.ts`, create the activity, record `sync_imports`. Unknown users get a 200 (same don't-reveal-existence behavior as Wahoo). The endpoint must return 200 quickly — Garmin retries on failure and slow consumers get throttled — so FIT download + conversion runs as a pg-boss job (`garmin-import-activity`), mirroring the federation lesson that synchronous work in inbound webhooks is a trap.
|
||||||
|
|
||||||
|
**Alternative considered:** synchronous processing in the webhook (Wahoo does this today). Rejected for Garmin: backfill bursts deliver many notifications at once; a queue absorbs the burst and gives retries for transient FIT-download failures.
|
||||||
|
|
||||||
|
### Decision: backfill-request UX instead of a Wahoo-style pick list
|
||||||
|
|
||||||
|
Garmin has no list endpoint, so the import page (`/settings/connections/garmin/import`) is a **date-range backfill requester**: pick a range (chunked into Garmin's per-request window under the hood), submit, and watch activities arrive. Progress is honest-async: the page shows requested ranges and the count of activities imported so far (rows in `sync_imports` attributed to the backfill window), with a note that Garmin delivers asynchronously and large histories take time. No per-activity pick list, no "Import all" button — those concepts don't exist in a push model. Per-activity dedupe stays in `sync_imports`, so re-requesting an overlapping range is safe and cheap.
|
||||||
|
|
||||||
|
**Alternative considered:** caching a local index of summaries first and offering selection before FIT download. Rejected for v1 — it doubles the moving parts for marginal value (users overwhelmingly want "import everything since X"); selective deletion after import already exists.
|
||||||
|
|
||||||
|
### Decision: deregistration notifications flip status to `revoked`
|
||||||
|
|
||||||
|
Garmin's deregistration notification deletes nothing locally except the live link: we set `status = 'revoked'` on the `connected_services` row (the existing audit-retaining state), surface the standard re-connect prompt, and stop all Garmin API calls for that user. Imported activities stay — they're the user's data in their journal, consistent with the disconnect semantics in the connected-services spec. User-initiated *deletion* of imported content remains the existing per-activity/account deletion paths.
|
||||||
|
|
||||||
|
### Decision: fixtures-first development; Garmin program approval gates rollout, not build
|
||||||
|
|
||||||
|
The importer/webhook are built and tested against recorded notification + FIT fixtures (same pattern as Wahoo's tests), with the live integration soaked once program credentials exist. Webhook endpoint registration in Garmin's portal targets staging first (`staging.trails.cool`), then production — both registrable under one app. Secrets follow the SOPS + compose pattern (`GARMIN_CLIENT_ID`, `GARMIN_CLIENT_SECRET`).
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- **Garmin program approval is external and slow** → fixtures-first build; the change is implementable and testable end-to-end without live credentials; rollout tasks are explicitly separated and can wait on Garmin without blocking merge (everything ships dark behind missing env vars, same as Wahoo on self-hosted instances).
|
||||||
|
- **Webhook delivery is best-effort; missed notifications mean missed activities** → backfill request doubles as a manual repair tool ("re-request last 7 days"); `sync_imports` dedupe makes overlap free.
|
||||||
|
- **Backfill bursts can spike load** → pg-boss queue with bounded concurrency for FIT download/conversion; webhook handler itself stays O(1).
|
||||||
|
- **Garmin rate limits (especially evaluation keys)** → respect 429/Retry-After in the job's retry policy; keep per-user backfill ranges chunked.
|
||||||
|
- **Ping `callbackURL` is attacker-controllable input if we blindly fetch it** → validate the callback host against Garmin's API host allowlist before fetching (SSRF guard — the federation work demonstrated exactly why).
|
||||||
|
- **Notification payload shapes are under-documented and shift between API versions** → normalize early into one internal shape with tolerant parsing; unknown notification types are logged and dropped (200), never 5xx.
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
1. Everything lands dark: no env vars → Garmin row hidden/disabled on `/settings/connections` (same gating as Wahoo's missing-client-id state).
|
||||||
|
2. Garmin developer program application (operator task, can run in parallel with implementation).
|
||||||
|
3. Staging: set secrets, register staging webhook URL in the Garmin portal, soak with a real Garmin account (connect → record activity → auto-import; backfill a month; revoke from Garmin side → `revoked`).
|
||||||
|
4. Production: same flag/secret pattern; privacy manifest entry ships with the code.
|
||||||
|
5. Rollback: remove secrets (provider hides), or disconnect-only — schema is untouched, so nothing to unwind.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
- Does our Garmin app get approved for the Activity API at production scale, and on what timeline? (Gates rollout tasks only.)
|
||||||
|
- Exact backfill chunk limit and burst rate for evaluation vs production keys — confirm against the current Activity API docs when credentials arrive; the chunking constant is one number in the manifest.
|
||||||
|
- Whether Garmin's notification includes enough summary data (duration/distance/type) to create stat-only activities for FIT-less entries (e.g. manually-logged workouts) — decide ingest-or-skip when fixtures are in hand; spec says ingest stats-only if the data is present, mirroring Wahoo's no-file behavior.
|
||||||
30
openspec/changes/garmin-import/proposal.md
Normal file
30
openspec/changes/garmin-import/proposal.md
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
## Why
|
||||||
|
|
||||||
|
Garmin is the largest fitness-device ecosystem our users own — and the most-requested missing import path. The connected-services framework was explicitly built so a second OAuth provider slots in behind the existing seams (`Importer`, `WebhookReceiver`, shared FIT→GPX), and after Wahoo and Komoot shipped, Garmin is the natural next provider to prove the framework holds for a push-first API.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- New provider `garmin` under `apps/journal/app/lib/connected-services/providers/garmin/` — manifest, OAuth2 (PKCE) config, `Importer` + `WebhookReceiver` capability adapters. Registered in the provider registry; settings UI, OAuth flow, and webhook routing light up without framework changes.
|
||||||
|
- Connect/disconnect Garmin from `/settings/connections` via Garmin's OAuth2 + PKCE flow; tokens stored as `credential_kind = 'oauth'` in `connected_services`.
|
||||||
|
- Automatic import of new activities via Garmin's **push model**: Garmin POSTs ping/push notifications to `/api/sync/webhook/garmin`; we fetch the FIT file from the notification's callback URL, convert via the shared `fit.ts`, and create the journal activity (idempotent via `sync_imports`).
|
||||||
|
- Historical import via Garmin's **backfill API**: unlike Wahoo, Garmin has no "list activities" endpoint — the import page requests backfill for a date range and the activities arrive asynchronously through the same webhook pipeline, with progress surfaced on the import page.
|
||||||
|
- Mandatory **deregistration handling**: Garmin sends a deregistration notification when a user revokes access from their end; we must mark the connection `revoked` (Garmin's terms additionally require ceasing data pulls for that user).
|
||||||
|
- Route push to Garmin devices (Courses API) is **out of scope** — it's a separate `RoutePusher` change once import has soaked, mirroring how `wahoo-route-push` followed `wahoo-import`.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
- `garmin-import`: Garmin Connect as an activity-import provider — OAuth2/PKCE connection, push-notification ingestion, date-range backfill for history, FIT conversion via the shared module, deregistration handling.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
<!-- none — the connected-services framework requirements already cover a new provider registering manifest + capability adapters; nothing framework-level changes. That holding true is part of the point of this change. -->
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- **Code**: new `providers/garmin/` (manifest, importer, webhook, tests); one import line in `providers/index.ts`. Import page route for Garmin (backfill-request UX differs from Wahoo's pick-list, so it's a provider-specific page, not a reuse of the Wahoo one). i18n strings (en + de).
|
||||||
|
- **APIs**: `/api/sync/connect/garmin`, `/api/sync/disconnect/garmin`, `/api/sync/webhook/garmin` — all already routed generically by provider name.
|
||||||
|
- **Schema**: none. `connected_services` (`credential_kind = 'oauth'`) and `sync_imports` cover Garmin as-is.
|
||||||
|
- **Secrets/Env**: `GARMIN_CLIENT_ID`, `GARMIN_CLIENT_SECRET` (SOPS + compose wiring, same pattern as Wahoo).
|
||||||
|
- **External dependency / risk**: requires an approved **Garmin Connect Developer Program** application. Evaluation keys are rate-limited and production use needs Garmin's review. This gates rollout, not development — the importer/webhook can be built and tested against recorded fixtures first. Webhook endpoints must also be registered in Garmin's developer portal (not self-service via API).
|
||||||
|
- **Privacy manifest**: `/legal/privacy` gains a Garmin entry alongside Wahoo/Komoot (what we pull, what Garmin learns, deletion semantics).
|
||||||
90
openspec/changes/garmin-import/specs/garmin-import/spec.md
Normal file
90
openspec/changes/garmin-import/specs/garmin-import/spec.md
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Connect Garmin account
|
||||||
|
Users SHALL be able to connect their Garmin account via OAuth2 with PKCE. The provider SHALL be registered through the connected-services manifest/registry seams; the settings page, OAuth routes, and webhook routing SHALL require no framework changes.
|
||||||
|
|
||||||
|
#### Scenario: Connect Garmin
|
||||||
|
- **WHEN** a user clicks "Connect Garmin" on `/settings/connections`
|
||||||
|
- **THEN** they are redirected to Garmin's OAuth authorization page with a PKCE code challenge
|
||||||
|
- **AND** after granting permission they are redirected back to the journal
|
||||||
|
- **AND** access and refresh tokens are stored in `connected_services` with `provider = 'garmin'`, `credential_kind = 'oauth'`
|
||||||
|
- **AND** the Garmin user id is stored as `provider_user_id` for webhook resolution
|
||||||
|
|
||||||
|
#### Scenario: Disconnect Garmin
|
||||||
|
- **WHEN** a user clicks "Disconnect" next to their Garmin connection
|
||||||
|
- **THEN** the stored credentials are removed and previously imported activities are unaffected
|
||||||
|
|
||||||
|
#### Scenario: Token refresh
|
||||||
|
- **WHEN** a Garmin API call is attempted with an expired access token
|
||||||
|
- **THEN** the OAuth `CredentialAdapter` refreshes it via `ConnectedServiceManager.withFreshCredentials`
|
||||||
|
- **AND** a permanently failed refresh (revoked refresh token) flips the connection to `needs_relink`
|
||||||
|
|
||||||
|
#### Scenario: Provider hidden without credentials
|
||||||
|
- **WHEN** the instance has no `GARMIN_CLIENT_ID` configured
|
||||||
|
- **THEN** the Garmin row on `/settings/connections` is absent or disabled (self-hosted instances without a Garmin program key see no broken flows)
|
||||||
|
|
||||||
|
### Requirement: Push-notification activity import
|
||||||
|
New Garmin activities SHALL be imported automatically via Garmin's notification pipeline. The webhook endpoint SHALL accept both ping notifications (payload fetched from the notification's callback URL) and push notifications (payload inline), normalize them into a single internal shape, and process imports asynchronously via a background job so the webhook itself responds quickly.
|
||||||
|
|
||||||
|
#### Scenario: New activity notification imports the activity
|
||||||
|
- **WHEN** Garmin POSTs an activity notification to `/api/sync/webhook/garmin`
|
||||||
|
- **THEN** the endpoint responds 200 promptly and enqueues an import job
|
||||||
|
- **AND** the job resolves the user via `provider_user_id`, downloads the FIT file, converts it via the shared FIT→GPX module, and creates a journal activity with GPX, stats, and PostGIS geometry
|
||||||
|
- **AND** the import is recorded in `sync_imports` to prevent duplicates
|
||||||
|
|
||||||
|
#### Scenario: Duplicate notification
|
||||||
|
- **WHEN** a notification arrives for an activity already recorded in `sync_imports`
|
||||||
|
- **THEN** the import is skipped silently (idempotent)
|
||||||
|
|
||||||
|
#### Scenario: Unknown user notification
|
||||||
|
- **WHEN** a notification arrives whose Garmin user id matches no active connection
|
||||||
|
- **THEN** the request is acknowledged with 200 and no data is processed (do not reveal user existence)
|
||||||
|
|
||||||
|
#### Scenario: Callback URL host validation
|
||||||
|
- **WHEN** a ping notification carries a callback URL whose host is not on the Garmin API host allowlist
|
||||||
|
- **THEN** the notification is dropped and logged; the URL is never fetched (SSRF guard)
|
||||||
|
|
||||||
|
#### Scenario: Activity without FIT data
|
||||||
|
- **WHEN** a notification describes an activity with no fetchable FIT file but includes summary stats
|
||||||
|
- **THEN** the activity is created without GPX or geometry (stats only), mirroring the established FIT-less behavior
|
||||||
|
|
||||||
|
#### Scenario: Unknown notification type
|
||||||
|
- **WHEN** Garmin delivers a notification type the handler does not recognize
|
||||||
|
- **THEN** it is logged and acknowledged with 200, never an error status
|
||||||
|
|
||||||
|
### Requirement: Historical import via backfill
|
||||||
|
Users SHALL be able to import their Garmin history by requesting backfill for a date range. Because Garmin exposes no activity-list endpoint, the import page SHALL present a date-range request flow with asynchronous progress, not a per-activity pick list. Range requests SHALL be chunked to Garmin's per-request window, and overlapping or repeated requests SHALL be safe (deduplicated via `sync_imports`).
|
||||||
|
|
||||||
|
#### Scenario: Request a backfill
|
||||||
|
- **WHEN** a connected user submits a date range on the Garmin import page
|
||||||
|
- **THEN** the system issues Garmin backfill requests covering the range in API-sized chunks
|
||||||
|
- **AND** the page records the requested range and shows it as in progress
|
||||||
|
|
||||||
|
#### Scenario: Backfilled activities arrive
|
||||||
|
- **WHEN** Garmin asynchronously delivers historical activities through the notification pipeline
|
||||||
|
- **THEN** each is imported by the same job path as live notifications
|
||||||
|
- **AND** the import page reflects the growing count of imported activities for the requested range
|
||||||
|
|
||||||
|
#### Scenario: Overlapping backfill request
|
||||||
|
- **WHEN** a user requests a range overlapping previously imported activities
|
||||||
|
- **THEN** already-imported activities are skipped via `sync_imports` and no duplicates are created
|
||||||
|
|
||||||
|
### Requirement: Deregistration handling
|
||||||
|
The system SHALL act on Garmin deregistration notifications. When a user revokes access on Garmin's side, the connection row SHALL be set to `status = 'revoked'`, all Garmin API calls for that user SHALL cease, and the user SHALL see the standard re-connect prompt. Imported activities SHALL be retained (they are the user's journal data; deletion remains the user's existing per-activity and account deletion paths).
|
||||||
|
|
||||||
|
#### Scenario: User revokes from Garmin's side
|
||||||
|
- **WHEN** a deregistration notification for a connected user arrives at the Garmin webhook
|
||||||
|
- **THEN** the `connected_services` row is set to `revoked`
|
||||||
|
- **AND** subsequent scheduled or user-triggered Garmin calls for that user short-circuit
|
||||||
|
- **AND** `/settings/connections` shows Garmin as requiring reconnection
|
||||||
|
|
||||||
|
### Requirement: Import provenance
|
||||||
|
Activities imported from Garmin SHALL show their origin, consistent with other providers.
|
||||||
|
|
||||||
|
#### Scenario: View imported activity
|
||||||
|
- **WHEN** a user views an activity imported from Garmin
|
||||||
|
- **THEN** an "Imported from garmin" badge is displayed on the detail page
|
||||||
|
|
||||||
|
#### Scenario: Delete and reimport
|
||||||
|
- **WHEN** a user deletes a Garmin-imported activity
|
||||||
|
- **THEN** the corresponding `sync_imports` record is deleted, so a future overlapping backfill can re-import it
|
||||||
42
openspec/changes/garmin-import/tasks.md
Normal file
42
openspec/changes/garmin-import/tasks.md
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
# Tasks — garmin-import
|
||||||
|
|
||||||
|
## 1. Provider scaffold + OAuth
|
||||||
|
|
||||||
|
- [ ] 1.1 Create `providers/garmin/manifest.ts`: `credential_kind = 'oauth'`, Garmin OAuth2 endpoints, scopes, env-gated `GARMIN_CLIENT_ID`/`GARMIN_CLIENT_SECRET`; register via `providers/index.ts`
|
||||||
|
- [ ] 1.2 PKCE support in the Garmin authorize/exchange flow: generate code verifier, carry it through `oauth-state.server.ts` alongside the state nonce, send `code_challenge`/`code_verifier` on authorize/exchange (token *storage and refresh* stay on the existing oauth `CredentialAdapter`)
|
||||||
|
- [ ] 1.3 Capture the Garmin user id at connect time and store as `provider_user_id`
|
||||||
|
- [ ] 1.4 `/settings/connections` shows Garmin connect/disconnect; row hidden/disabled when env vars are absent (mirror Wahoo's gating); i18n strings (en + de)
|
||||||
|
- [ ] 1.5 Unit tests: manifest registration, PKCE parameter generation/verification, connect flow state round-trip
|
||||||
|
|
||||||
|
## 2. Webhook ingestion
|
||||||
|
|
||||||
|
- [ ] 2.1 `WebhookReceiver` adapter for `/api/sync/webhook/garmin`: accept POST, normalize ping (fetch callback URL) and push (inline payload) into one internal activity-notification shape, respond 200 fast
|
||||||
|
- [ ] 2.2 Callback-URL host allowlist (Garmin API hosts only) — drop + log anything else; never fetch unvalidated URLs (SSRF guard)
|
||||||
|
- [ ] 2.3 pg-boss job `garmin-import-activity`: resolve user via `provider_user_id`, fetch FIT via `withFreshCredentials`, convert via shared `fit.ts`, create activity (GPX + stats + geometry; stats-only when no FIT), record `sync_imports`; idempotent on duplicate notifications; respect 429/Retry-After in retry policy
|
||||||
|
- [ ] 2.4 Unknown-user notifications → 200, no processing; unknown notification types → log + 200
|
||||||
|
- [ ] 2.5 Tests with recorded notification + FIT fixtures: ping path, push path, dedupe, unknown user, SSRF rejection, FIT-less stats-only
|
||||||
|
|
||||||
|
## 3. Backfill (historical import)
|
||||||
|
|
||||||
|
- [ ] 3.1 Backfill request client: chunk a user-supplied date range into Garmin's per-request window; issue requests via `withFreshCredentials`
|
||||||
|
- [ ] 3.2 Import page `/settings/connections/garmin/import`: date-range form, list of requested ranges, count of imported activities per range, honest async messaging; i18n (en + de)
|
||||||
|
- [ ] 3.3 Persist backfill requests (range, requested_at, user) so progress survives reloads — smallest storage that works (reuse existing tables if possible; new table only if not)
|
||||||
|
- [ ] 3.4 Tests: chunking math, overlap re-request safety (dedupe via `sync_imports`), progress counting
|
||||||
|
|
||||||
|
## 4. Deregistration + lifecycle
|
||||||
|
|
||||||
|
- [ ] 4.1 Handle Garmin deregistration notifications: set `connected_services.status = 'revoked'`, short-circuit subsequent Garmin calls, surface the standard re-connect prompt
|
||||||
|
- [ ] 4.2 Tests: deregistration flips status; revoked connection blocks backfill/import paths
|
||||||
|
|
||||||
|
## 5. Provenance + docs
|
||||||
|
|
||||||
|
- [ ] 5.1 "Imported from garmin" badge works via the existing provenance path; delete-and-reimport clears `sync_imports`
|
||||||
|
- [ ] 5.2 Privacy manifest entry on `/legal/privacy` (en legal-manifest + de legal section): what we pull from Garmin, what Garmin learns, deletion/revocation semantics
|
||||||
|
- [ ] 5.3 Secrets wiring: `GARMIN_CLIENT_ID`/`GARMIN_CLIENT_SECRET` in SOPS `secrets.app.env`, compose env (prod + staging), `.env.example` notes
|
||||||
|
|
||||||
|
## 6. Rollout (gated on Garmin developer program approval)
|
||||||
|
|
||||||
|
- [ ] 6.1 Apply to the Garmin Connect Developer Program (operator); record approved scopes/limits and the confirmed backfill chunk window in design.md
|
||||||
|
- [ ] 6.2 Register staging webhook URL in the Garmin portal; soak on staging with a real device/account: connect → record → auto-import; backfill a month; revoke from Garmin side → `revoked`
|
||||||
|
- [ ] 6.3 Register production webhook URL; set production secrets; verify end-to-end on trails.cool
|
||||||
|
- [ ] 6.4 Resolve the design open question on FIT-less notifications (ingest stats-only vs skip) against real fixtures; update spec annotation if behavior differs
|
||||||
Loading…
Add table
Add a link
Reference in a new issue