From afdb8163ce91da9571b463656119f1bfb9a2f76b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 4 Nov 2026 22:03:08 +0100 Subject: [PATCH] fix(onboarding): offer Garmin as a source The setup question about where readings come from listed every other connection on Settings and not Garmin, so the flow could neither record it nor end on connecting it. Garmin is now a Q4 source and a browser connectable one: the key is pasted on its card, so nothing can be missing up front. A guard now pins that every connection the health read covers is either an onboarding source or named with the reason it is not. Refs #1333 --- CHANGELOG.md | 3 ++ docs/api/openapi.yaml | 4 +- messages/de.json | 1 + messages/en.json | 1 + messages/es.json | 1 + messages/fr.json | 1 + messages/it.json | 1 + messages/ko.json | 1 + messages/nl.json | 1 + messages/pl.json | 1 + messages/pt-BR.json | 1 + messages/ru.json | 1 + .../__tests__/first-result-config.test.ts | 42 +++++++++++++++++++ src/lib/onboarding/first-result-config.ts | 9 +++- src/lib/onboarding/needs.ts | 1 + src/lib/onboarding/wizard-steps.ts | 1 + 16 files changed, 68 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45d96f795..bd8159d1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,9 @@ cycles like a period logged by hand, a finished Garmin period also marks its last day, and periods imported earlier are repaired once on the next start. A day you logged yourself is never changed. (#1333) +- **Garmin in the setup questions.** The onboarding question about where + your readings come from now offers Garmin, and can end on connecting it. + (#1333) - **Typing into the smoking history.** "When you stopped" only took text that was already a whole YYYY-MM month, so every keystroke was dropped and only a paste got in. The start year clamped each keystroke into its diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 1d896a3de..eaf513ca2 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -36694,7 +36694,7 @@ components: type: string const: done sources: - maxItems: 10 + maxItems: 11 type: array items: type: string @@ -36705,6 +36705,7 @@ components: - oura - whoop - polar + - garmin - fitbit - strava - nightscout @@ -83103,6 +83104,7 @@ components: - oura - whoop - polar + - garmin - fitbit - strava - nightscout diff --git a/messages/de.json b/messages/de.json index 5abe7c515..addd1369c 100644 --- a/messages/de.json +++ b/messages/de.json @@ -5277,6 +5277,7 @@ "oura": "Oura", "whoop": "WHOOP", "polar": "Polar", + "garmin": "Garmin", "fitbit": "Fitbit", "strava": "Strava", "nightscout": "Nightscout", diff --git a/messages/en.json b/messages/en.json index 4160fe8fd..6eecabb83 100644 --- a/messages/en.json +++ b/messages/en.json @@ -5277,6 +5277,7 @@ "oura": "Oura", "whoop": "WHOOP", "polar": "Polar", + "garmin": "Garmin", "fitbit": "Fitbit", "strava": "Strava", "nightscout": "Nightscout", diff --git a/messages/es.json b/messages/es.json index aff7c3bca..663d66f6e 100644 --- a/messages/es.json +++ b/messages/es.json @@ -5277,6 +5277,7 @@ "oura": "Oura", "whoop": "WHOOP", "polar": "Polar", + "garmin": "Garmin", "fitbit": "Fitbit", "strava": "Strava", "nightscout": "Nightscout", diff --git a/messages/fr.json b/messages/fr.json index 348f3d5a4..50e0eb018 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -5277,6 +5277,7 @@ "oura": "Oura", "whoop": "WHOOP", "polar": "Polar", + "garmin": "Garmin", "fitbit": "Fitbit", "strava": "Strava", "nightscout": "Nightscout", diff --git a/messages/it.json b/messages/it.json index ad13867cc..c05731311 100644 --- a/messages/it.json +++ b/messages/it.json @@ -5277,6 +5277,7 @@ "oura": "Oura", "whoop": "WHOOP", "polar": "Polar", + "garmin": "Garmin", "fitbit": "Fitbit", "strava": "Strava", "nightscout": "Nightscout", diff --git a/messages/ko.json b/messages/ko.json index 98499067d..10ebcb44e 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -5277,6 +5277,7 @@ "oura": "Oura", "whoop": "WHOOP", "polar": "Polar", + "garmin": "Garmin", "fitbit": "Fitbit", "strava": "Strava", "nightscout": "Nightscout", diff --git a/messages/nl.json b/messages/nl.json index 96d427f61..a53d0187c 100644 --- a/messages/nl.json +++ b/messages/nl.json @@ -5277,6 +5277,7 @@ "oura": "Oura", "whoop": "WHOOP", "polar": "Polar", + "garmin": "Garmin", "fitbit": "Fitbit", "strava": "Strava", "nightscout": "Nightscout", diff --git a/messages/pl.json b/messages/pl.json index d5fb8cd97..c9b05a0c0 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -5277,6 +5277,7 @@ "oura": "Oura", "whoop": "WHOOP", "polar": "Polar", + "garmin": "Garmin", "fitbit": "Fitbit", "strava": "Strava", "nightscout": "Nightscout", diff --git a/messages/pt-BR.json b/messages/pt-BR.json index 7436334e1..eb89e8d03 100644 --- a/messages/pt-BR.json +++ b/messages/pt-BR.json @@ -5277,6 +5277,7 @@ "oura": "Oura", "whoop": "WHOOP", "polar": "Polar", + "garmin": "Garmin", "fitbit": "Fitbit", "strava": "Strava", "nightscout": "Nightscout", diff --git a/messages/ru.json b/messages/ru.json index 7ef35a608..6563de908 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -5277,6 +5277,7 @@ "oura": "Oura", "whoop": "WHOOP", "polar": "Polar", + "garmin": "Garmin", "fitbit": "Fitbit", "strava": "Strava", "nightscout": "Nightscout", diff --git a/src/lib/onboarding/__tests__/first-result-config.test.ts b/src/lib/onboarding/__tests__/first-result-config.test.ts index 5eebd8325..d05ee5f12 100644 --- a/src/lib/onboarding/__tests__/first-result-config.test.ts +++ b/src/lib/onboarding/__tests__/first-result-config.test.ts @@ -8,6 +8,7 @@ import { join } from "node:path"; import { describe, expect, it } from "vitest"; +import { CONNECTION_KEYS } from "@/lib/integrations/connection-health"; import { ONBOARDING_AREA_KEYS } from "@/lib/modules/registry"; import { NAV_DESTINATIONS } from "@/components/layout/nav-model"; @@ -18,8 +19,22 @@ import { deliveringSources, SOURCE_INTEGRATION, } from "../first-result-config"; +import { ONBOARDING_SOURCE_KEYS } from "../needs"; import { BROWSER_CONNECTABLE_SOURCES } from "../wizard-steps"; +/** + * Connections the setup flow deliberately does not ask about, each with its + * reason. Every other connection must be a Q4 source; a provider shipped on + * Settings and missing here was absent from the flow without anybody deciding + * it. + */ +const NOT_AN_ONBOARDING_SOURCE: Record = { + "google-health": + "the successor transport behind the Fitbit answer, which still links the classic Fitbit card", + dawarich: + "movement context for the environment module, not a source of readings", +}; + describe("first-result targets", () => { it("covers every area with exactly one of a form or a page", () => { for (const area of ONBOARDING_AREA_KEYS) { @@ -36,6 +51,33 @@ describe("first-result targets", () => { } }); + it("asks about every connection Settings offers, or says why not", () => { + const sources = new Set(ONBOARDING_SOURCE_KEYS); + const missing = CONNECTION_KEYS.filter( + (key) => !sources.has(key) && !(key in NOT_AN_ONBOARDING_SOURCE), + ); + expect(missing).toEqual([]); + for (const key of Object.keys(NOT_AN_ONBOARDING_SOURCE)) { + expect(CONNECTION_KEYS as readonly string[]).toContain(key); + } + }); + + it("offers Garmin as a source the browser connects with a pasted key", () => { + expect(BROWSER_CONNECTABLE_SOURCES).toContain("garmin"); + expect(SOURCE_INTEGRATION.garmin).toEqual({ + anchor: "garmin", + statusKey: "garmin", + credentials: "self-hosted", + }); + const view = connectSourceView("garmin", { + connected: false, + configured: false, + syncHealth: { verdict: "disconnected", since: null }, + lastSuccessAt: null, + }); + expect(view?.slot).toBe("connect"); + }); + it("names an integration for every source the browser can connect", () => { for (const source of BROWSER_CONNECTABLE_SOURCES) { expect(SOURCE_INTEGRATION[source].anchor).toBeTruthy(); diff --git a/src/lib/onboarding/first-result-config.ts b/src/lib/onboarding/first-result-config.ts index 304755c59..9c033b761 100644 --- a/src/lib/onboarding/first-result-config.ts +++ b/src/lib/onboarding/first-result-config.ts @@ -61,7 +61,8 @@ export const AREA_PAGE_HREF: Readonly< * a dead end. * - `shared` — an operator env app is the fallback (`available` says whether * any usable credentials resolve, own or shared). - * - `self-hosted` — a URL the person pastes; nothing can be missing up front. + * - `self-hosted` — a URL or a key the person pastes; nothing can be missing + * up front. */ export type SourceCredentialModel = "byo" | "shared" | "self-hosted"; @@ -80,6 +81,12 @@ export const SOURCE_INTEGRATION: Readonly< oura: { anchor: "oura", statusKey: "oura", credentials: "shared" }, whoop: { anchor: "whoop", statusKey: "whoop", credentials: "byo" }, polar: { anchor: "polar", statusKey: "polar", credentials: "shared" }, + // The person's own Personal API key, pasted on the card. + garmin: { + anchor: "garmin", + statusKey: "garmin", + credentials: "self-hosted", + }, fitbit: { anchor: "fitbit", statusKey: "fitbit", credentials: "byo" }, strava: { anchor: "strava", statusKey: "strava", credentials: "shared" }, nightscout: { diff --git a/src/lib/onboarding/needs.ts b/src/lib/onboarding/needs.ts index 5ab044d73..a319b918b 100644 --- a/src/lib/onboarding/needs.ts +++ b/src/lib/onboarding/needs.ts @@ -99,6 +99,7 @@ export const ONBOARDING_SOURCE_KEYS = [ "oura", "whoop", "polar", + "garmin", "fitbit", "strava", "nightscout", diff --git a/src/lib/onboarding/wizard-steps.ts b/src/lib/onboarding/wizard-steps.ts index d84b8adb4..8f411d264 100644 --- a/src/lib/onboarding/wizard-steps.ts +++ b/src/lib/onboarding/wizard-steps.ts @@ -34,6 +34,7 @@ export const BROWSER_CONNECTABLE_SOURCES = [ "oura", "whoop", "polar", + "garmin", "fitbit", "strava", "nightscout",