Files
HealthLog/prisma/migrations/0492_garmin_personal_api/migration.sql
T
Marc-André Bombeck e2ba86b12f feat(garmin): sync Garmin Connect through the Personal API
Garmin Connect now offers a Personal API: a person creates a key for
their own account and any client can buy short-lived tokens with it.
HealthLog takes that key in a new Garmin card, checks it with Garmin
before storing it sealed on the user row, and polls every hour.

- Transport: key exchange with a per-process token cache keyed on the
  key, one re-exchange on a refused token and then reauth_required, the
  429 Retry-After carried to the caller, the China host tried when the
  global one does not know the key, answers read with a byte cap, all
  egress through safeFetch to the two fixed hosts.
- Sync: daily summaries, sleep summaries, HRV, weigh-ins and the
  activity search, chunked to each resource's documented range, read one
  after another and stopped at the first rate limit; today's menstrual
  cycle summary adds logged period days the person has not logged.
- Backfill: a resumable walk through the shared admission lane that
  learns where the history starts from the daily summaries and the
  activities, waits out Garmin's rate limit and stamps its marker.
- Types: GARMIN source; BODY_BATTERY and STRESS_LEVEL for Garmin's own
  day scores, kept apart from the computed STRESS_SCORE; TRAINING_READINESS
  and ACUTE_TRAINING_LOAD for figures a bridge pushes through the ingest
  token. All four chart on the recovery page and sit on the ladders,
  units, ranges, labels and report groups like the device-native scores.
- Key expiry: learned from a token capped short of a day, else estimated
  from the validity picked at connect; the card warns a week ahead and
  asks for a new key once Garmin refuses the old one.
- Ledger, health, freshness, watchdog, backup classes, wipe plan,
  encrypted-column registry, rotation script, OpenAPI and all ten
  locales carry the new integration. The old "no Garmin connector" note
  is gone.
2026-11-04 10:14:10 +01:00

35 lines
1.7 KiB
SQL

-- Garmin Personal API (#1333): a per-user key pasted once, a new measurement
-- source for the rows the sync writes, Garmin's own day scores (Body Battery,
-- stress level), and two types a bridge pushes through the ingest token
-- because the Personal API does not carry them (Training Readiness, acute
-- training load).
--
-- One DO block, so the lock wait is bounded and the file is one statement
-- that rolls back whole. Adding an enum value inside a transaction is fine
-- since PostgreSQL 12 as long as the value is not used in it, and nothing
-- here uses them.
--
-- Purely additive: nullable columns, no default, no backfill. IF NOT EXISTS
-- keeps a replay harmless. Enum values cannot be dropped in PostgreSQL, so
-- there is no down path for them; dropping the six columns undoes the rest.
--
-- @retry-on-lock-timeout
DO $migration$
BEGIN
PERFORM set_config('lock_timeout', '5s', true);
ALTER TYPE "measurement_source" ADD VALUE IF NOT EXISTS 'GARMIN';
ALTER TYPE "measurement_type" ADD VALUE IF NOT EXISTS 'BODY_BATTERY';
ALTER TYPE "measurement_type" ADD VALUE IF NOT EXISTS 'STRESS_LEVEL';
ALTER TYPE "measurement_type" ADD VALUE IF NOT EXISTS 'TRAINING_READINESS';
ALTER TYPE "measurement_type" ADD VALUE IF NOT EXISTS 'ACUTE_TRAINING_LOAD';
ALTER TABLE "users"
ADD COLUMN IF NOT EXISTS "garmin_api_key_encrypted" TEXT,
ADD COLUMN IF NOT EXISTS "garmin_key_expires_at" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "garmin_connected_at" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "garmin_region" TEXT,
ADD COLUMN IF NOT EXISTS "garmin_backfill_progress" JSONB,
ADD COLUMN IF NOT EXISTS "garmin_backfill_completed_at" TIMESTAMP(3);
END
$migration$;