fix(google-health): surface the connection status and add the resume and test routes

Wire the settings card to the live connection so it reflects reality
after a connect.

- Emit a google-health entry on the consolidated integration-status
  envelope (configured, connected, last sync, backfill, and the
  needs-reconnect flag). Without it the card read an undefined status and
  showed a permanently disconnected connection even after a successful
  OAuth grant, hiding the sync, disconnect, and reconnect controls.
- Add the resume and test-connection routes the card calls, mirroring the
  classic Fitbit ones.
- Use the shared refresh icon on the reconnect action.
- Drop the experimental-scope opt-in from the runbook and describe ECG
  and irregular-rhythm as a planned addition rather than a current toggle.
This commit is contained in:
Marc-André Bombeck committed 2026-07-03 14:41:53 +02:00
1 parent 422a6a7df0
commit 0d3067837e
6 files changed
+241 -14

No files matched your search

@@ -11,6 +11,7 @@ vi.mock("@/lib/db", () => ({
withingsConnection: { findUnique: vi.fn(async () => null) },
whoopConnection: { findUnique: vi.fn(async () => null) },
fitbitConnection: { findUnique: vi.fn(async () => null) },
googleHealthConnection: { findUnique: vi.fn(async () => null) },
moodEntry: { count: vi.fn(async () => 0) },
},
}));
+51
View File
@@ -40,12 +40,14 @@ export const GET = apiHandler(async () => {
moodLogStatus,
whoopStatus,
fitbitStatus,
googleHealthStatus,
polarStatus,
ouraStatus,
dbUser,
withingsConn,
whoopConn,
fitbitConn,
googleHealthConn,
moodLogEntryCount,
// v1.17.1 — `available` reports whether usable OAuth credentials resolve
// (per-user BYO first, then the shared env app), mirroring the per-card
@@ -57,6 +59,7 @@ export const GET = apiHandler(async () => {
getIntegrationStatus(user.id, "moodlog"),
getIntegrationStatus(user.id, "whoop"),
getIntegrationStatus(user.id, "fitbit"),
getIntegrationStatus(user.id, "google-health"),
getIntegrationStatus(user.id, "polar"),
getIntegrationStatus(user.id, "oura"),
prisma.user.findUnique({
@@ -68,6 +71,8 @@ export const GET = apiHandler(async () => {
whoopClientSecretEncrypted: true,
fitbitClientIdEncrypted: true,
fitbitClientSecretEncrypted: true,
googleHealthClientIdEncrypted: true,
googleHealthClientSecretEncrypted: true,
polarAccessTokenEncrypted: true,
polarClientIdEncrypted: true,
polarClientSecretEncrypted: true,
@@ -108,6 +113,16 @@ export const GET = apiHandler(async () => {
backfillCompletedAt: true,
},
}),
prisma.googleHealthConnection.findUnique({
where: { userId: user.id },
select: {
tokenExpiresAt: true,
lastSyncedAt: true,
createdAt: true,
backfillCompletedAt: true,
needsReauth: true,
},
}),
// v1.7.0 sync — exclude tombstoned rows from the entry count.
prisma.moodEntry.count({
where: { userId: user.id, deletedAt: null },
@@ -183,6 +198,27 @@ export const GET = apiHandler(async () => {
: null,
backfillCompleted: fitbitConn ? !!fitbitConn.backfillCompletedAt : null,
} satisfies IntegrationViewModel & FitbitExtras,
{
...googleHealthStatus,
configured:
!!dbUser?.googleHealthClientIdEncrypted &&
!!dbUser?.googleHealthClientSecretEncrypted,
connected: !!googleHealthConn,
connectedAt: googleHealthConn?.createdAt?.toISOString() ?? null,
legacyLastSyncedAt:
googleHealthConn?.lastSyncedAt?.toISOString() ?? null,
tokenExpiresAt: googleHealthConn?.tokenExpiresAt?.toISOString() ?? null,
tokenExpired: googleHealthConn
? googleHealthConn.tokenExpiresAt.getTime() <= now
: null,
backfillCompleted: googleHealthConn
? !!googleHealthConn.backfillCompletedAt
: null,
// v1.27.0 — `needsReauth` is the 7-day Testing-mode refresh expiry (or a
// user-revoked grant) surfaced from the connection row; the card reads it
// to raise a distinct "Reconnect" CTA separate from parked/disconnected.
needsReauth: googleHealthConn ? googleHealthConn.needsReauth : false,
} satisfies IntegrationViewModel & GoogleHealthExtras,
{
...polarStatus,
// `connected` = a stored access token; `configured` mirrors it (the
@@ -256,6 +292,21 @@ interface FitbitExtras {
backfillCompleted: boolean | null;
}
// v1.27.0 — Google Health mirrors the Fitbit shape plus `needsReauth`: Google
// expires the refresh token after 7 days in "Testing" publishing mode, so a
// connected user is periodically pushed back through OAuth. The card reads the
// flag to raise a distinct reconnect banner.
interface GoogleHealthExtras {
configured: boolean;
connected: boolean;
connectedAt: string | null;
legacyLastSyncedAt: string | null;
tokenExpiresAt: string | null;
tokenExpired: boolean | null;
backfillCompleted: boolean | null;
needsReauth: boolean;
}
// v1.17.1 — Polar / Oura fold into the consolidated envelope. They carry the
// per-user BYO-key flags the shared OAuth card reads instead of the dedicated
// /api/<provider>/status round-trip the page used to fire per card.