From 633a451c1656daa57927e89f138dd044580b2268 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Sun, 14 Jun 2026 21:11:15 +0200
Subject: [PATCH] feat(integrations): match parked, test, and data-link
treatment on new cards
---
messages/de.json | 3 +
messages/en.json | 3 +
messages/es.json | 3 +
messages/fr.json | 3 +
messages/it.json | 3 +
messages/pl.json | 3 +
.../__tests__/nightscout-card.test.tsx | 85 ++++++++++
.../__tests__/oauth-provider-card.test.tsx | 88 +++++++++++
.../settings/integrations/nightscout-card.tsx | 145 +++++++++++++-----
.../integrations/oauth-provider-card.tsx | 123 +++++++++++----
.../settings/integrations/oura-card.tsx | 4 +
.../settings/integrations/polar-card.tsx | 4 +
12 files changed, 396 insertions(+), 71 deletions(-)
create mode 100644 src/components/settings/integrations/__tests__/nightscout-card.test.tsx
create mode 100644 src/components/settings/integrations/__tests__/oauth-provider-card.test.tsx
diff --git a/messages/de.json b/messages/de.json
index da188c5c9..0e16cf538 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -4570,6 +4570,7 @@
"nightscout": "Nightscout",
"nightscoutTag": "CGM",
"nightscoutDescription": "Verbinde deine eigene Nightscout-Seite, um kontinuierliche Glukosewerte für das Glukose-Panel zu synchronisieren.",
+ "nightscoutViewData": "Glukose ansehen",
"nightscoutHelp": "Richte HealthLog auf deine Nightscout-Instanz aus. Werte werden stündlich im Hintergrund abgerufen.",
"nightscoutUrl": "Nightscout-URL",
"nightscoutUrlSavedPlaceholder": "Gespeichert — neue URL eingeben zum Ersetzen",
@@ -4588,6 +4589,7 @@
"nightscoutDisconnectDescription": "Künftige Synchronisierungen stoppen. Bereits importierte Glukosewerte bleiben in deinem Verlauf.",
"polar": "Polar",
"polarDescription": "Verbinde dein Polar-Konto, um Nightly-Recharge-Erholung, Schlafphasen und Tagesaktivität zu synchronisieren.",
+ "polarViewData": "Schlaf & Erholung ansehen",
"polarHelp": "Melde dich bei Polar an, um den Zugriff zu erlauben. Daten werden stündlich im Hintergrund abgerufen.",
"polarUnavailable": "Polar ist auf diesem Server nicht konfiguriert. Bitte deinen Administrator, die Polar-OAuth-Zugangsdaten zu hinterlegen.",
"polarConnect": "Polar verbinden",
@@ -4608,6 +4610,7 @@
},
"oura": "Oura",
"ouraDescription": "Verbinde dein Oura-Konto, um Readiness, Schlafphasen, HRV und Tagesaktivität zu synchronisieren.",
+ "ouraViewData": "Schlaf & Erholung ansehen",
"ouraHelp": "Melde dich bei Oura an, um den Zugriff zu erlauben. Daten werden stündlich im Hintergrund abgerufen.",
"ouraUnavailable": "Oura ist auf diesem Server nicht konfiguriert. Bitte deinen Administrator, die Oura-OAuth-Zugangsdaten zu hinterlegen.",
"ouraConnect": "Oura verbinden",
diff --git a/messages/en.json b/messages/en.json
index 162ac5504..68afa4acc 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -4570,6 +4570,7 @@
"nightscout": "Nightscout",
"nightscoutTag": "CGM",
"nightscoutDescription": "Connect your own Nightscout site to sync continuous glucose readings for the glucose panel.",
+ "nightscoutViewData": "View your glucose",
"nightscoutHelp": "Point HealthLog at your Nightscout instance. Readings are pulled in the background once an hour.",
"nightscoutUrl": "Nightscout URL",
"nightscoutUrlSavedPlaceholder": "Saved — enter a new URL to replace it",
@@ -4588,6 +4589,7 @@
"nightscoutDisconnectDescription": "Future syncs stop. Glucose readings already imported stay in your history.",
"polar": "Polar",
"polarDescription": "Connect your Polar account to sync Nightly Recharge recovery, sleep stages, and daily activity.",
+ "polarViewData": "View your sleep & recovery",
"polarHelp": "Sign in with Polar to grant access. Data is pulled in the background once an hour.",
"polarUnavailable": "Polar is not configured on this server. Ask your administrator to add the Polar OAuth credentials.",
"polarConnect": "Connect Polar",
@@ -4608,6 +4610,7 @@
},
"oura": "Oura",
"ouraDescription": "Connect your Oura account to sync readiness, sleep stages, HRV, and daily activity.",
+ "ouraViewData": "View your sleep & recovery",
"ouraHelp": "Sign in with Oura to grant access. Data is pulled in the background once an hour.",
"ouraUnavailable": "Oura is not configured on this server. Ask your administrator to add the Oura OAuth credentials.",
"ouraConnect": "Connect Oura",
diff --git a/messages/es.json b/messages/es.json
index ac43c3815..ac2bf14e3 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -4570,6 +4570,7 @@
"nightscout": "Nightscout",
"nightscoutTag": "CGM",
"nightscoutDescription": "Conecta tu propio sitio Nightscout para sincronizar lecturas continuas de glucosa en el panel de glucosa.",
+ "nightscoutViewData": "Ver tu glucosa",
"nightscoutHelp": "Apunta HealthLog a tu instancia de Nightscout. Las lecturas se obtienen en segundo plano una vez por hora.",
"nightscoutUrl": "URL de Nightscout",
"nightscoutUrlSavedPlaceholder": "Guardado: introduce una URL nueva para reemplazarla",
@@ -4588,6 +4589,7 @@
"nightscoutDisconnectDescription": "Las sincronizaciones futuras se detienen. Las lecturas de glucosa ya importadas permanecen en tu historial.",
"polar": "Polar",
"polarDescription": "Conecta tu cuenta de Polar para sincronizar la recuperación Nightly Recharge, las fases de sueño y la actividad diaria.",
+ "polarViewData": "Ver tu sueño y recuperación",
"polarHelp": "Inicia sesión en Polar para conceder acceso. Los datos se obtienen en segundo plano una vez por hora.",
"polarUnavailable": "Polar no está configurado en este servidor. Pide a tu administrador que añada las credenciales OAuth de Polar.",
"polarConnect": "Conectar Polar",
@@ -4608,6 +4610,7 @@
},
"oura": "Oura",
"ouraDescription": "Conecta tu cuenta de Oura para sincronizar readiness, fases de sueño, VFC y actividad diaria.",
+ "ouraViewData": "Ver tu sueño y recuperación",
"ouraHelp": "Inicia sesión en Oura para conceder acceso. Los datos se obtienen en segundo plano una vez por hora.",
"ouraUnavailable": "Oura no está configurado en este servidor. Pide a tu administrador que añada las credenciales OAuth de Oura.",
"ouraConnect": "Conectar Oura",
diff --git a/messages/fr.json b/messages/fr.json
index 1f8eaa98e..182ec080b 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -4570,6 +4570,7 @@
"nightscout": "Nightscout",
"nightscoutTag": "CGM",
"nightscoutDescription": "Connectez votre propre site Nightscout pour synchroniser les mesures de glycémie en continu dans le panneau glucose.",
+ "nightscoutViewData": "Voir votre glycémie",
"nightscoutHelp": "Pointez HealthLog vers votre instance Nightscout. Les mesures sont récupérées en arrière-plan une fois par heure.",
"nightscoutUrl": "URL Nightscout",
"nightscoutUrlSavedPlaceholder": "Enregistré — saisissez une nouvelle URL pour la remplacer",
@@ -4588,6 +4589,7 @@
"nightscoutDisconnectDescription": "Les synchronisations futures s'arrêtent. Les mesures de glycémie déjà importées restent dans votre historique.",
"polar": "Polar",
"polarDescription": "Connectez votre compte Polar pour synchroniser la récupération Nightly Recharge, les phases de sommeil et l'activité quotidienne.",
+ "polarViewData": "Voir votre sommeil et récupération",
"polarHelp": "Connectez-vous à Polar pour accorder l'accès. Les données sont récupérées en arrière-plan une fois par heure.",
"polarUnavailable": "Polar n'est pas configuré sur ce serveur. Demandez à votre administrateur d'ajouter les identifiants OAuth Polar.",
"polarConnect": "Connecter Polar",
@@ -4608,6 +4610,7 @@
},
"oura": "Oura",
"ouraDescription": "Connectez votre compte Oura pour synchroniser le readiness, les phases de sommeil, la VFC et l'activité quotidienne.",
+ "ouraViewData": "Voir votre sommeil et récupération",
"ouraHelp": "Connectez-vous à Oura pour accorder l'accès. Les données sont récupérées en arrière-plan une fois par heure.",
"ouraUnavailable": "Oura n'est pas configuré sur ce serveur. Demandez à votre administrateur d'ajouter les identifiants OAuth Oura.",
"ouraConnect": "Connecter Oura",
diff --git a/messages/it.json b/messages/it.json
index bc5a81c1c..4d833acfb 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -4570,6 +4570,7 @@
"nightscout": "Nightscout",
"nightscoutTag": "CGM",
"nightscoutDescription": "Collega il tuo sito Nightscout per sincronizzare le letture continue della glicemia nel pannello glicemia.",
+ "nightscoutViewData": "Vedi la tua glicemia",
"nightscoutHelp": "Punta HealthLog alla tua istanza Nightscout. Le letture vengono recuperate in background una volta all'ora.",
"nightscoutUrl": "URL Nightscout",
"nightscoutUrlSavedPlaceholder": "Salvato — inserisci un nuovo URL per sostituirlo",
@@ -4588,6 +4589,7 @@
"nightscoutDisconnectDescription": "Le sincronizzazioni future si interrompono. Le letture della glicemia già importate restano nella tua cronologia.",
"polar": "Polar",
"polarDescription": "Collega il tuo account Polar per sincronizzare il recupero Nightly Recharge, le fasi del sonno e l'attività giornaliera.",
+ "polarViewData": "Vedi sonno e recupero",
"polarHelp": "Accedi a Polar per concedere l'accesso. I dati vengono recuperati in background una volta all'ora.",
"polarUnavailable": "Polar non è configurato su questo server. Chiedi al tuo amministratore di aggiungere le credenziali OAuth di Polar.",
"polarConnect": "Collega Polar",
@@ -4608,6 +4610,7 @@
},
"oura": "Oura",
"ouraDescription": "Collega il tuo account Oura per sincronizzare readiness, fasi del sonno, HRV e attività giornaliera.",
+ "ouraViewData": "Vedi sonno e recupero",
"ouraHelp": "Accedi a Oura per concedere l'accesso. I dati vengono recuperati in background una volta all'ora.",
"ouraUnavailable": "Oura non è configurato su questo server. Chiedi al tuo amministratore di aggiungere le credenziali OAuth di Oura.",
"ouraConnect": "Collega Oura",
diff --git a/messages/pl.json b/messages/pl.json
index e66363b37..13147adea 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -4570,6 +4570,7 @@
"nightscout": "Nightscout",
"nightscoutTag": "CGM",
"nightscoutDescription": "Połącz własną witrynę Nightscout, aby synchronizować ciągłe odczyty glukozy w panelu glukozy.",
+ "nightscoutViewData": "Zobacz swoją glukozę",
"nightscoutHelp": "Wskaż HealthLog na swoją instancję Nightscout. Odczyty są pobierane w tle raz na godzinę.",
"nightscoutUrl": "Adres URL Nightscout",
"nightscoutUrlSavedPlaceholder": "Zapisano — wpisz nowy adres URL, aby go zastąpić",
@@ -4588,6 +4589,7 @@
"nightscoutDisconnectDescription": "Przyszłe synchronizacje zostaną zatrzymane. Już zaimportowane odczyty glukozy pozostaną w historii.",
"polar": "Polar",
"polarDescription": "Połącz konto Polar, aby synchronizować regenerację Nightly Recharge, fazy snu i aktywność dzienną.",
+ "polarViewData": "Zobacz sen i regenerację",
"polarHelp": "Zaloguj się w Polar, aby przyznać dostęp. Dane są pobierane w tle raz na godzinę.",
"polarUnavailable": "Polar nie jest skonfigurowany na tym serwerze. Poproś administratora o dodanie danych OAuth Polar.",
"polarConnect": "Połącz Polar",
@@ -4608,6 +4610,7 @@
},
"oura": "Oura",
"ouraDescription": "Połącz konto Oura, aby synchronizować readiness, fazy snu, HRV i aktywność dzienną.",
+ "ouraViewData": "Zobacz sen i regenerację",
"ouraHelp": "Zaloguj się w Oura, aby przyznać dostęp. Dane są pobierane w tle raz na godzinę.",
"ouraUnavailable": "Oura nie jest skonfigurowana na tym serwerze. Poproś administratora o dodanie danych OAuth Oura.",
"ouraConnect": "Połącz Oura",
diff --git a/src/components/settings/integrations/__tests__/nightscout-card.test.tsx b/src/components/settings/integrations/__tests__/nightscout-card.test.tsx
new file mode 100644
index 000000000..6b5a6b0ac
--- /dev/null
+++ b/src/components/settings/integrations/__tests__/nightscout-card.test.tsx
@@ -0,0 +1,85 @@
+/**
+ * v1.17.0 — interaction-parity + correctness checks for the Nightscout card.
+ *
+ * 1. A `parked` status renders the warning banner + reconnect button
+ * (matching the WHOOP card's parked treatment, byte-for-byte classes).
+ * 2. A `connected` status renders the shared TestConnectionButton AND the
+ * connect→data link to /insights/blood-glucose.
+ * 3. The disconnect mutation invalidates `nightscoutStatus()` — the key the
+ * status query actually reads — so the pill refreshes after disconnect.
+ */
+import { describe, expect, it, vi } from "vitest";
+import { renderToStaticMarkup } from "react-dom/server";
+
+import { queryKeys } from "@/lib/query-keys";
+
+// Capture the disconnect mutation's onSuccess so the test can assert which
+// query keys it invalidates. Status is driven off a per-test payload.
+let statusPayload: unknown = null;
+const invalidateSpy = vi.fn();
+type OnSuccess = () => void;
+const capturedDisconnectOnSuccess: { fn: OnSuccess | null } = { fn: null };
+
+vi.mock("@tanstack/react-query", () => ({
+ useQuery: () => ({ data: statusPayload, isLoading: false }),
+ useMutation: ({ onSuccess }: { onSuccess?: () => void }) => {
+ // The Nightscout card declares exactly one mutation (disconnect).
+ capturedDisconnectOnSuccess.fn = onSuccess ?? null;
+ return { mutate: vi.fn(), isPending: false };
+ },
+ useQueryClient: () => ({ invalidateQueries: invalidateSpy }),
+}));
+
+import { I18nProvider } from "@/lib/i18n/context";
+import { NightscoutCard } from "../nightscout-card";
+
+function render() {
+ return renderToStaticMarkup(
+
+
+ ,
+ );
+}
+
+describe("NightscoutCard — parked + test + data-link + invalidation", () => {
+ it("renders the parked banner + reconnect button when state is parked", () => {
+ statusPayload = {
+ connected: true,
+ configured: true,
+ state: "parked",
+ lastSuccessAt: null,
+ lastError: "Nightscout unreachable",
+ };
+ const html = render();
+ expect(html).toContain('data-state="parked"');
+ expect(html).toContain('data-testid="nightscout-parked-banner"');
+ expect(html).toContain('data-testid="nightscout-resume-button"');
+ expect(html).toContain("border-warning/30 bg-warning/10");
+ });
+
+ it("renders the test-connection button + data link when connected", () => {
+ statusPayload = {
+ connected: true,
+ configured: true,
+ state: "connected",
+ lastSuccessAt: "2026-06-01T00:00:00.000Z",
+ lastError: null,
+ };
+ const html = render();
+ expect(html).toContain("Test connection");
+ expect(html).toContain('data-testid="nightscout-data-link"');
+ expect(html).toContain('href="/insights/blood-glucose"');
+ });
+
+ it("disconnect invalidates the status key the query reads (nightscoutStatus)", () => {
+ statusPayload = { connected: true, configured: true, state: "connected" };
+ invalidateSpy.mockClear();
+ capturedDisconnectOnSuccess.fn = null;
+ render();
+ const onSuccess = capturedDisconnectOnSuccess.fn as (() => void) | null;
+ expect(typeof onSuccess).toBe("function");
+ onSuccess?.();
+ const invalidatedKeys = invalidateSpy.mock.calls.map((c) => c[0]?.queryKey);
+ expect(invalidatedKeys).toContainEqual(queryKeys.nightscoutStatus());
+ });
+});
diff --git a/src/components/settings/integrations/__tests__/oauth-provider-card.test.tsx b/src/components/settings/integrations/__tests__/oauth-provider-card.test.tsx
new file mode 100644
index 000000000..1e6b801dd
--- /dev/null
+++ b/src/components/settings/integrations/__tests__/oauth-provider-card.test.tsx
@@ -0,0 +1,88 @@
+/**
+ * v1.17.0 — interaction-parity check for the shared OAuth integration card
+ * (Polar / Oura). The new cards must match the existing WHOOP card's
+ * parked-state + test-connection + connect→data treatment:
+ *
+ * 1. A `parked` status renders the warning banner + reconnect button.
+ * 2. A `connected` status renders the shared TestConnectionButton (its
+ * "Test connection" affordance) AND the connect→data link.
+ * 3. The data link resolves to the provider's insight surface.
+ */
+import { describe, expect, it, vi } from "vitest";
+import { renderToStaticMarkup } from "react-dom/server";
+import { Watch } from "lucide-react";
+
+// Drive the card's own `useQuery` status read off a per-test payload.
+let statusPayload: unknown = null;
+vi.mock("@tanstack/react-query", () => ({
+ useQuery: () => ({ data: statusPayload, isLoading: false }),
+ useMutation: () => ({ mutate: vi.fn(), isPending: false }),
+ useQueryClient: () => ({ invalidateQueries: vi.fn() }),
+}));
+
+import { I18nProvider } from "@/lib/i18n/context";
+import { OAuthProviderCard } from "../oauth-provider-card";
+
+function render() {
+ return renderToStaticMarkup(
+
+
+ ,
+ );
+}
+
+describe("OAuthProviderCard — parked + test + data-link parity", () => {
+ it("renders the parked banner + reconnect button when state is parked", () => {
+ statusPayload = {
+ connected: true,
+ configured: true,
+ available: true,
+ state: "parked",
+ lastSuccessAt: null,
+ lastError: "Polar grant expired",
+ };
+ const html = render();
+ expect(html).toContain('data-state="parked"');
+ expect(html).toContain('data-testid="polar-parked-banner"');
+ expect(html).toContain('data-testid="polar-resume-button"');
+ expect(html).toContain("reconnect manually");
+ // The parked banner uses the same warning treatment as the WHOOP card.
+ expect(html).toContain("border-warning/30 bg-warning/10");
+ });
+
+ it("renders the test-connection button + data link when connected", () => {
+ statusPayload = {
+ connected: true,
+ configured: true,
+ available: true,
+ state: "connected",
+ lastSuccessAt: "2026-06-01T00:00:00.000Z",
+ lastError: null,
+ };
+ const html = render();
+ // The shared TestConnectionButton surfaces its "Test connection" label.
+ expect(html).toContain("Test connection");
+ // connect→data link points at the provider's insight surface.
+ expect(html).toContain('data-testid="polar-data-link"');
+ expect(html).toContain('href="/insights/sleep"');
+ });
+
+ it("does not render the data link or test button when disconnected", () => {
+ statusPayload = {
+ connected: false,
+ configured: false,
+ available: true,
+ };
+ const html = render();
+ expect(html).not.toContain('data-testid="polar-data-link"');
+ expect(html).not.toContain("Test connection");
+ // The connect CTA stands in instead.
+ expect(html).toContain('data-testid="polar-connect"');
+ });
+});
diff --git a/src/components/settings/integrations/nightscout-card.tsx b/src/components/settings/integrations/nightscout-card.tsx
index c42bbfc91..97edbc3e7 100644
--- a/src/components/settings/integrations/nightscout-card.tsx
+++ b/src/components/settings/integrations/nightscout-card.tsx
@@ -7,9 +7,10 @@
// consolidated envelope. Warm copy, mobile-first, the private-network opt-in
// toggle maps to `nightscoutAllowPrivateHost`.
-import { useState } from "react";
+import { useRef, useState } from "react";
+import Link from "next/link";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-import { Activity, Link2, Loader2, Save, Unlink } from "lucide-react";
+import { ArrowRight, Droplet, Link2, Loader2, Save, Unlink } from "lucide-react";
import {
AlertDialog,
@@ -32,6 +33,7 @@ import {
IntegrationStatusPill,
type IntegrationPillState,
} from "@/components/settings/integration-status-pill";
+import { TestConnectionButton } from "@/components/settings/test-connection-button";
import { apiFetchRaw, apiGet, apiPost } from "@/lib/api/api-fetch";
import { useTranslations } from "@/lib/i18n/context";
import { invalidateKeys, measurementDependentKeys, queryKeys } from "@/lib/query-keys";
@@ -82,6 +84,8 @@ export function NightscoutCard({ enabled = true }: { enabled?: boolean }) {
refetchOnWindowFocus: true,
});
+ const formRef = useRef(null);
+
const disconnect = useMutation({
mutationFn: async () => {
await apiPost("/api/nightscout/disconnect");
@@ -90,7 +94,15 @@ export function NightscoutCard({ enabled = true }: { enabled?: boolean }) {
setUrl("");
setToken("");
setAllowPrivateHost(false);
- queryClient.invalidateQueries({ queryKey: queryKeys.nightscout() });
+ // The status read keys on `nightscoutStatus()`; invalidate it so the
+ // pill flips back after a disconnect (matching the Withings / WHOOP
+ // pattern), alongside the cross-integration envelope.
+ queryClient.invalidateQueries({
+ queryKey: queryKeys.nightscoutStatus(),
+ });
+ queryClient.invalidateQueries({
+ queryKey: queryKeys.integrationsStatus(),
+ });
},
});
@@ -114,7 +126,12 @@ export function NightscoutCard({ enabled = true }: { enabled?: boolean }) {
setMsgType("success");
setToken("");
void invalidateKeys(queryClient, measurementDependentKeys);
- queryClient.invalidateQueries({ queryKey: queryKeys.nightscout() });
+ queryClient.invalidateQueries({
+ queryKey: queryKeys.nightscoutStatus(),
+ });
+ queryClient.invalidateQueries({
+ queryKey: queryKeys.integrationsStatus(),
+ });
} else {
let detail = t("settings.nightscoutConnectFailed");
try {
@@ -145,7 +162,7 @@ export function NightscoutCard({ enabled = true }: { enabled?: boolean }) {
className="bg-card border-border rounded-xl border p-4 sm:p-6"
>
@@ -166,11 +183,46 @@ export function NightscoutCard({ enabled = true }: { enabled?: boolean }) {
{errorMessage && }
+ {/* Parked-integration resume CTA. Surfaces only when the row state
+ is `parked` (>24h of persistent failures). Nightscout has no OAuth
+ redirect — the user re-validates by re-submitting the connect form,
+ so "reconnect" scrolls the form into view + focuses the URL field.
+ Markup matches the WHOOP card byte for byte. */}
+ {pillState === "parked" && (
+
diff --git a/src/components/settings/integrations/oauth-provider-card.tsx b/src/components/settings/integrations/oauth-provider-card.tsx
index 42f68950a..ea783d179 100644
--- a/src/components/settings/integrations/oauth-provider-card.tsx
+++ b/src/components/settings/integrations/oauth-provider-card.tsx
@@ -8,8 +8,9 @@
// Mirrors the Nightscout card's self-contained status pattern.
import { useState } from "react";
+import Link from "next/link";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-import { Activity, Link2, Unlink } from "lucide-react";
+import { ArrowRight, Link2, Unlink, type LucideIcon } from "lucide-react";
import {
AlertDialog,
@@ -28,6 +29,7 @@ import {
IntegrationStatusPill,
type IntegrationPillState,
} from "@/components/settings/integration-status-pill";
+import { TestConnectionButton } from "@/components/settings/test-connection-button";
import { apiGet, apiPost } from "@/lib/api/api-fetch";
import { useTranslations } from "@/lib/i18n/context";
import { invalidateKeys, measurementDependentKeys } from "@/lib/query-keys";
@@ -68,6 +70,10 @@ export interface OAuthProviderCardProps {
statusQueryKey: readonly unknown[];
/** i18n key prefix (e.g. `settings.polar`). */
i18nPrefix: string;
+ /** Distinct card glyph, kept in the same size/treatment as the other cards. */
+ icon: LucideIcon;
+ /** Where this provider's synced data surfaces (e.g. `/insights/sleep`). */
+ dataHref: string;
enabled?: boolean;
}
@@ -75,6 +81,8 @@ export function OAuthProviderCard({
provider,
statusQueryKey,
i18nPrefix,
+ icon,
+ dataHref,
enabled = true,
}: OAuthProviderCardProps) {
const { t } = useTranslations();
@@ -116,7 +124,7 @@ export function OAuthProviderCard({
className="bg-card border-border rounded-xl border p-4 sm:p-6"
>
{t(`${i18nPrefix}Description`)}
}
status={
@@ -140,6 +148,36 @@ export function OAuthProviderCard({
)}
+ {/* Parked-integration resume CTA. Surfaces only when the row state
+ is `parked` (>24h of persistent failures). For an env-based OAuth
+ provider there are no stored BYO credentials to re-validate — the
+ grant itself has to be re-issued, so "reconnect" re-initiates the
+ existing connect flow (`/api//connect`); a successful
+ callback clears the park. Markup matches the WHOOP card byte for
+ byte. */}
+ {pillState === "parked" && (
+