feat(fitbit): add the Google Health settings card with connection test and resume

Give the Google Health (Fitbit & Pixel) integration the same settings-card
parity WHOOP and Withings already have. It is a BYO-key provider — the user
pastes their own Google OAuth client id/secret, then connects — so the card
mirrors the WHOOP flow rather than the server-brokered Withings one.

- New POST /api/integrations/fitbit/test does a live, rate-limited
  (5/60s per user) probe against the Google Health profile endpoint via
  the refreshed token from getValidToken, returning { ok, latencyMs,
  lastSyncedAt } and the same error-code union WHOOP uses. The Bearer
  token and upstream host are scrubbed before any annotation.
- New POST /api/integrations/fitbit/resume clears a parked integration
  via resumeIntegrationFromPark, rate-limited to match the test route.
- FitbitCard renders the credentials form, OAuth connect, sync now /
  sync all data / test connection / disconnect action row, the parked
  resume banner, and a backfill-in-progress line. Status reads from the
  existing /api/fitbit/status; the pill/error/parked state comes off the
  cross-integration envelope, which now carries a fitbit entry.
- Card strings added under settings.fitbit* across all six locales.
This commit is contained in:
Marc-André Bombeck committed 2026-06-04 22:37:58 +02:00
1 parent 14469a6de9
commit f44dccb828
12 files changed
+1078 -40

No files matched your search

@@ -163,8 +163,9 @@ describe("IntegrationsSection — single-status-display contract (A5)", () => {
});
const html = render();
// Exactly one pill per card → 3 pills total (Withings, moodLog, WHOOP).
expect(count(html, 'data-testid="integration-status-pill"')).toBe(3);
// Exactly one pill per card → 4 pills total (Withings, moodLog, WHOOP,
// Fitbit / Google Health).
expect(count(html, 'data-testid="integration-status-pill"')).toBe(4);
// The redundant banner from v1.4.15 is gone.
expect(html).not.toContain('data-testid="integration-status-banner"');
// Card-body "letzter Sync" repetition is gone — no
@@ -354,7 +355,8 @@ describe("IntegrationsSection — single-status-display contract (A5)", () => {
const html = render();
// Every integration card includes the section divider data-testid so the
// header → body separation is visually consistent.
expect(count(html, 'data-testid="integration-card-divider"')).toBe(3);
// header → body separation is visually consistent (Withings, moodLog,
// WHOOP, Fitbit / Google Health).
expect(count(html, 'data-testid="integration-card-divider"')).toBe(4);
});
});