From 0bcbb88db11ae32eba002a841603d8c40cb8d347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 25 Jul 2026 04:43:12 +0200 Subject: [PATCH] feat(settings): put a sync-now button on the four cards that lacked one A route with no caller is an API detail, not a feature. The Oura, Polar, Strava and Nightscout cards get the same action WHOOP already had, in the same row as Test connection, rendered only once connected: post the sync, show the count or the failure, invalidate the reads that depend on measurements. No full-history dialog, since these providers have no such arm. Keys added across the six locales. --- messages/de.json | 12 ++++ messages/en.json | 12 ++++ messages/es.json | 12 ++++ messages/fr.json | 12 ++++ messages/it.json | 12 ++++ messages/pl.json | 12 ++++ .../__tests__/nightscout-card.test.tsx | 23 +++++++ .../__tests__/oauth-provider-card.test.tsx | 31 +++++++++ .../settings/integrations/nightscout-card.tsx | 64 +++++++++++++++++++ .../integrations/oauth-provider-card.tsx | 62 ++++++++++++++++++ 10 files changed, 252 insertions(+) diff --git a/messages/de.json b/messages/de.json index 3f965de1d..2a1b33f10 100644 --- a/messages/de.json +++ b/messages/de.json @@ -5543,6 +5543,9 @@ "nightscoutUpdate": "Änderungen speichern", "nightscoutConnected": "Nightscout verbunden.", "nightscoutConnectFailed": "Verbindung zu Nightscout fehlgeschlagen.", + "nightscoutSync": "Jetzt synchronisieren", + "nightscoutSyncResult": "{count} Messwerte synchronisiert", + "nightscoutSyncFailed": "Synchronisierung fehlgeschlagen", "nightscoutDisconnect": "Trennen", "nightscoutDisconnectTitle": "Nightscout trennen?", "nightscoutDisconnectDescription": "Künftige Synchronisierungen stoppen. Bereits importierte Glukosewerte bleiben in deinem Verlauf.", @@ -5558,6 +5561,9 @@ "polarSaveCredentials": "Speichern", "polarUnavailable": "Polar ist auf diesem Server nicht konfiguriert. Füge oben deine eigenen Polar-OAuth-Zugangsdaten hinzu oder bitte deinen Administrator, die gemeinsame App zu konfigurieren.", "polarConnect": "Polar verbinden", + "polarSync": "Jetzt synchronisieren", + "polarSyncResult": "{count} Messwerte synchronisiert", + "polarSyncFailed": "Synchronisierung fehlgeschlagen", "polarDisconnect": "Trennen", "polarDisconnectTitle": "Polar trennen?", "polarDisconnectDescription": "Künftige Synchronisierungen stoppen. Bereits importierte Werte bleiben in deinem Verlauf.", @@ -5585,6 +5591,9 @@ "ouraSaveCredentials": "Speichern", "ouraUnavailable": "Oura ist auf diesem Server nicht konfiguriert. Füge oben deine eigenen Oura-OAuth-Zugangsdaten hinzu oder bitte deinen Administrator, die gemeinsame App zu konfigurieren.", "ouraConnect": "Oura verbinden", + "ouraSync": "Jetzt synchronisieren", + "ouraSyncResult": "{count} Messwerte synchronisiert", + "ouraSyncFailed": "Synchronisierung fehlgeschlagen", "ouraDisconnect": "Trennen", "ouraDisconnectTitle": "Oura trennen?", "ouraDisconnectDescription": "Künftige Synchronisierungen stoppen. Bereits importierte Werte bleiben in deinem Verlauf.", @@ -5813,6 +5822,9 @@ "stravaSaveCredentials": "Speichern", "stravaUnavailable": "Strava ist auf diesem Server nicht konfiguriert. Füge oben deine eigenen Strava-API-Zugangsdaten hinzu oder bitte deinen Administrator, die gemeinsame App zu konfigurieren.", "stravaConnect": "Strava verbinden", + "stravaSync": "Jetzt synchronisieren", + "stravaSyncResult": "{count} Messwerte synchronisiert", + "stravaSyncFailed": "Synchronisierung fehlgeschlagen", "stravaDisconnect": "Trennen", "stravaDisconnectTitle": "Strava trennen?", "stravaDisconnectDescription": "Künftige Synchronisierungen stoppen. Bereits importierte Aktivitäten bleiben in deinem Verlauf.", diff --git a/messages/en.json b/messages/en.json index ef2007aae..de6e07aa0 100644 --- a/messages/en.json +++ b/messages/en.json @@ -5533,6 +5533,9 @@ "nightscoutUpdate": "Save changes", "nightscoutConnected": "Nightscout connected.", "nightscoutConnectFailed": "Could not connect to Nightscout.", + "nightscoutSync": "Sync now", + "nightscoutSyncResult": "{count} measurements synchronized", + "nightscoutSyncFailed": "Sync failed", "nightscoutDisconnect": "Disconnect", "nightscoutDisconnectTitle": "Disconnect Nightscout?", "nightscoutDisconnectDescription": "Future syncs stop. Glucose readings already imported stay in your history.", @@ -5548,6 +5551,9 @@ "polarSaveCredentials": "Save", "polarUnavailable": "Polar is not configured on this server. Add your own Polar OAuth credentials above, or ask your administrator to configure the shared app.", "polarConnect": "Connect Polar", + "polarSync": "Sync now", + "polarSyncResult": "{count} measurements synchronized", + "polarSyncFailed": "Sync failed", "polarDisconnect": "Disconnect", "polarDisconnectTitle": "Disconnect Polar?", "polarDisconnectDescription": "Future syncs stop. Readings already imported stay in your history.", @@ -5575,6 +5581,9 @@ "ouraSaveCredentials": "Save", "ouraUnavailable": "Oura is not configured on this server. Add your own Oura OAuth credentials above, or ask your administrator to configure the shared app.", "ouraConnect": "Connect Oura", + "ouraSync": "Sync now", + "ouraSyncResult": "{count} measurements synchronized", + "ouraSyncFailed": "Sync failed", "ouraDisconnect": "Disconnect", "ouraDisconnectTitle": "Disconnect Oura?", "ouraDisconnectDescription": "Future syncs stop. Readings already imported stay in your history.", @@ -5803,6 +5812,9 @@ "stravaSaveCredentials": "Save", "stravaUnavailable": "Strava is not configured on this server. Add your own Strava API credentials above, or ask your administrator to configure the shared app.", "stravaConnect": "Connect Strava", + "stravaSync": "Sync now", + "stravaSyncResult": "{count} measurements synchronized", + "stravaSyncFailed": "Sync failed", "stravaDisconnect": "Disconnect", "stravaDisconnectTitle": "Disconnect Strava?", "stravaDisconnectDescription": "Future syncs stop. Activities already imported stay in your history.", diff --git a/messages/es.json b/messages/es.json index c6c9f21a2..8d6bbbc5c 100644 --- a/messages/es.json +++ b/messages/es.json @@ -5545,6 +5545,9 @@ "nightscoutUpdate": "Guardar cambios", "nightscoutConnected": "Nightscout conectado.", "nightscoutConnectFailed": "No se pudo conectar con Nightscout.", + "nightscoutSync": "Sincronizar ahora", + "nightscoutSyncResult": "{count} mediciones sincronizadas", + "nightscoutSyncFailed": "Error de sincronización", "nightscoutDisconnect": "Desconectar", "nightscoutDisconnectTitle": "¿Desconectar Nightscout?", "nightscoutDisconnectDescription": "Las sincronizaciones futuras se detienen. Las lecturas de glucosa ya importadas permanecen en tu historial.", @@ -5560,6 +5563,9 @@ "polarSaveCredentials": "Guardar", "polarUnavailable": "Polar no está configurado en este servidor. Añade arriba tus propias credenciales OAuth de Polar, o pide a tu administrador que configure la app compartida.", "polarConnect": "Conectar Polar", + "polarSync": "Sincronizar ahora", + "polarSyncResult": "{count} mediciones sincronizadas", + "polarSyncFailed": "Error de sincronización", "polarDisconnect": "Desconectar", "polarDisconnectTitle": "¿Desconectar Polar?", "polarDisconnectDescription": "Las sincronizaciones futuras se detienen. Las lecturas ya importadas permanecen en tu historial.", @@ -5587,6 +5593,9 @@ "ouraSaveCredentials": "Guardar", "ouraUnavailable": "Oura no está configurado en este servidor. Añade arriba tus propias credenciales OAuth de Oura, o pide a tu administrador que configure la app compartida.", "ouraConnect": "Conectar Oura", + "ouraSync": "Sincronizar ahora", + "ouraSyncResult": "{count} mediciones sincronizadas", + "ouraSyncFailed": "Error de sincronización", "ouraDisconnect": "Desconectar", "ouraDisconnectTitle": "¿Desconectar Oura?", "ouraDisconnectDescription": "Las sincronizaciones futuras se detienen. Las lecturas ya importadas permanecen en tu historial.", @@ -5815,6 +5824,9 @@ "stravaSaveCredentials": "Guardar", "stravaUnavailable": "Strava no está configurado en este servidor. Añade arriba tus propias credenciales de API de Strava o pide a tu administrador que configure la app compartida.", "stravaConnect": "Conectar Strava", + "stravaSync": "Sincronizar ahora", + "stravaSyncResult": "{count} mediciones sincronizadas", + "stravaSyncFailed": "Error de sincronización", "stravaDisconnect": "Desconectar", "stravaDisconnectTitle": "¿Desconectar Strava?", "stravaDisconnectDescription": "Las sincronizaciones futuras se detienen. Las actividades ya importadas permanecen en tu historial.", diff --git a/messages/fr.json b/messages/fr.json index 870ed5dfd..afa537f60 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -5547,6 +5547,9 @@ "nightscoutUpdate": "Enregistrer les modifications", "nightscoutConnected": "Nightscout connecté.", "nightscoutConnectFailed": "Impossible de se connecter à Nightscout.", + "nightscoutSync": "Synchroniser", + "nightscoutSyncResult": "{count} mesures synchronisées", + "nightscoutSyncFailed": "Échec de la synchronisation", "nightscoutDisconnect": "Déconnecter", "nightscoutDisconnectTitle": "Déconnecter Nightscout ?", "nightscoutDisconnectDescription": "Les synchronisations futures s'arrêtent. Les mesures de glycémie déjà importées restent dans votre historique.", @@ -5562,6 +5565,9 @@ "polarSaveCredentials": "Enregistrer", "polarUnavailable": "Polar n'est pas configuré sur ce serveur. Ajoutez vos propres identifiants OAuth Polar ci-dessus, ou demandez à votre administrateur de configurer l'application partagée.", "polarConnect": "Connecter Polar", + "polarSync": "Synchroniser", + "polarSyncResult": "{count} mesures synchronisées", + "polarSyncFailed": "Échec de la synchronisation", "polarDisconnect": "Déconnecter", "polarDisconnectTitle": "Déconnecter Polar ?", "polarDisconnectDescription": "Les synchronisations futures s'arrêtent. Les mesures déjà importées restent dans votre historique.", @@ -5589,6 +5595,9 @@ "ouraSaveCredentials": "Enregistrer", "ouraUnavailable": "Oura n'est pas configuré sur ce serveur. Ajoutez vos propres identifiants OAuth Oura ci-dessus, ou demandez à votre administrateur de configurer l'application partagée.", "ouraConnect": "Connecter Oura", + "ouraSync": "Synchroniser", + "ouraSyncResult": "{count} mesures synchronisées", + "ouraSyncFailed": "Échec de la synchronisation", "ouraDisconnect": "Déconnecter", "ouraDisconnectTitle": "Déconnecter Oura ?", "ouraDisconnectDescription": "Les synchronisations futures s'arrêtent. Les mesures déjà importées restent dans votre historique.", @@ -5817,6 +5826,9 @@ "stravaSaveCredentials": "Enregistrer", "stravaUnavailable": "Strava n'est pas configuré sur ce serveur. Ajoute tes propres identifiants API Strava ci-dessus ou demande à ton administrateur de configurer l'application partagée.", "stravaConnect": "Connecter Strava", + "stravaSync": "Synchroniser", + "stravaSyncResult": "{count} mesures synchronisées", + "stravaSyncFailed": "Échec de la synchronisation", "stravaDisconnect": "Déconnecter", "stravaDisconnectTitle": "Déconnecter Strava ?", "stravaDisconnectDescription": "Les synchronisations futures s'arrêtent. Les activités déjà importées restent dans ton historique.", diff --git a/messages/it.json b/messages/it.json index c8eae2287..d29e1ff10 100644 --- a/messages/it.json +++ b/messages/it.json @@ -5537,6 +5537,9 @@ "nightscoutUpdate": "Salva modifiche", "nightscoutConnected": "Nightscout connesso.", "nightscoutConnectFailed": "Impossibile connettersi a Nightscout.", + "nightscoutSync": "Sincronizza ora", + "nightscoutSyncResult": "{count} misurazioni sincronizzate", + "nightscoutSyncFailed": "Sincronizzazione non riuscita", "nightscoutDisconnect": "Disconnetti", "nightscoutDisconnectTitle": "Disconnettere Nightscout?", "nightscoutDisconnectDescription": "Le sincronizzazioni future si interrompono. Le letture della glicemia già importate restano nella tua cronologia.", @@ -5552,6 +5555,9 @@ "polarSaveCredentials": "Salva", "polarUnavailable": "Polar non è configurato su questo server. Aggiungi sopra le tue credenziali OAuth di Polar oppure chiedi al tuo amministratore di configurare l'app condivisa.", "polarConnect": "Collega Polar", + "polarSync": "Sincronizza ora", + "polarSyncResult": "{count} misurazioni sincronizzate", + "polarSyncFailed": "Sincronizzazione non riuscita", "polarDisconnect": "Disconnetti", "polarDisconnectTitle": "Disconnettere Polar?", "polarDisconnectDescription": "Le sincronizzazioni future si interrompono. Le letture già importate restano nella tua cronologia.", @@ -5579,6 +5585,9 @@ "ouraSaveCredentials": "Salva", "ouraUnavailable": "Oura non è configurato su questo server. Aggiungi sopra le tue credenziali OAuth di Oura oppure chiedi al tuo amministratore di configurare l'app condivisa.", "ouraConnect": "Collega Oura", + "ouraSync": "Sincronizza ora", + "ouraSyncResult": "{count} misurazioni sincronizzate", + "ouraSyncFailed": "Sincronizzazione non riuscita", "ouraDisconnect": "Disconnetti", "ouraDisconnectTitle": "Disconnettere Oura?", "ouraDisconnectDescription": "Le sincronizzazioni future si interrompono. Le letture già importate restano nella tua cronologia.", @@ -5807,6 +5816,9 @@ "stravaSaveCredentials": "Salva", "stravaUnavailable": "Strava non è configurato su questo server. Aggiungi sopra le tue credenziali API di Strava o chiedi al tuo amministratore di configurare l'app condivisa.", "stravaConnect": "Collega Strava", + "stravaSync": "Sincronizza ora", + "stravaSyncResult": "{count} misurazioni sincronizzate", + "stravaSyncFailed": "Sincronizzazione non riuscita", "stravaDisconnect": "Disconnetti", "stravaDisconnectTitle": "Disconnettere Strava?", "stravaDisconnectDescription": "Le sincronizzazioni future si interrompono. Le attività già importate restano nella cronologia.", diff --git a/messages/pl.json b/messages/pl.json index 0ea309978..390c23307 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -5531,6 +5531,9 @@ "nightscoutUpdate": "Zapisz zmiany", "nightscoutConnected": "Nightscout połączony.", "nightscoutConnectFailed": "Nie można połączyć z Nightscout.", + "nightscoutSync": "Synchronizuj teraz", + "nightscoutSyncResult": "Zsynchronizowano {count} pomiarów", + "nightscoutSyncFailed": "Synchronizacja nie powiodła się", "nightscoutDisconnect": "Rozłącz", "nightscoutDisconnectTitle": "Rozłączyć Nightscout?", "nightscoutDisconnectDescription": "Przyszłe synchronizacje zostaną zatrzymane. Już zaimportowane odczyty glukozy pozostaną w historii.", @@ -5546,6 +5549,9 @@ "polarSaveCredentials": "Zapisz", "polarUnavailable": "Polar nie jest skonfigurowany na tym serwerze. Dodaj powyżej własne dane OAuth Polar lub poproś administratora o skonfigurowanie współdzielonej aplikacji.", "polarConnect": "Połącz Polar", + "polarSync": "Synchronizuj teraz", + "polarSyncResult": "Zsynchronizowano {count} pomiarów", + "polarSyncFailed": "Synchronizacja nie powiodła się", "polarDisconnect": "Rozłącz", "polarDisconnectTitle": "Rozłączyć Polar?", "polarDisconnectDescription": "Przyszłe synchronizacje zostaną zatrzymane. Już zaimportowane odczyty pozostaną w historii.", @@ -5573,6 +5579,9 @@ "ouraSaveCredentials": "Zapisz", "ouraUnavailable": "Oura nie jest skonfigurowana na tym serwerze. Dodaj powyżej własne dane OAuth Oura lub poproś administratora o skonfigurowanie współdzielonej aplikacji.", "ouraConnect": "Połącz Oura", + "ouraSync": "Synchronizuj teraz", + "ouraSyncResult": "Zsynchronizowano {count} pomiarów", + "ouraSyncFailed": "Synchronizacja nie powiodła się", "ouraDisconnect": "Rozłącz", "ouraDisconnectTitle": "Rozłączyć Oura?", "ouraDisconnectDescription": "Przyszłe synchronizacje zostaną zatrzymane. Już zaimportowane odczyty pozostaną w historii.", @@ -5801,6 +5810,9 @@ "stravaSaveCredentials": "Zapisz", "stravaUnavailable": "Strava nie jest skonfigurowana na tym serwerze. Dodaj powyżej własne dane API Strava lub poproś administratora o skonfigurowanie wspólnej aplikacji.", "stravaConnect": "Połącz Strava", + "stravaSync": "Synchronizuj teraz", + "stravaSyncResult": "Zsynchronizowano {count} pomiarów", + "stravaSyncFailed": "Synchronizacja nie powiodła się", "stravaDisconnect": "Rozłącz", "stravaDisconnectTitle": "Rozłączyć Strava?", "stravaDisconnectDescription": "Przyszłe synchronizacje zostaną zatrzymane. Już zaimportowane aktywności pozostaną w historii.", diff --git a/src/components/settings/integrations/__tests__/nightscout-card.test.tsx b/src/components/settings/integrations/__tests__/nightscout-card.test.tsx index 6b5a6b0ac..4259839dc 100644 --- a/src/components/settings/integrations/__tests__/nightscout-card.test.tsx +++ b/src/components/settings/integrations/__tests__/nightscout-card.test.tsx @@ -83,3 +83,26 @@ describe("NightscoutCard — parked + test + data-link + invalidation", () => { expect(invalidatedKeys).toContainEqual(queryKeys.nightscoutStatus()); }); }); + +/** + * v1.32.28 — sync now, matching the treatment the OAuth cards get. + */ +describe("NightscoutCard — sync-now action", () => { + it("offers the sync action on a connected instance", () => { + statusPayload = { + connected: true, + configured: true, + state: "connected", + lastSuccessAt: null, + lastError: null, + }; + const html = render(); + expect(html).toContain('data-testid="nightscout-sync"'); + expect(html).toContain("Sync now"); + }); + + it("does not offer it before an instance is configured", () => { + statusPayload = { connected: false, configured: false }; + expect(render()).not.toContain('data-testid="nightscout-sync"'); + }); +}); diff --git a/src/components/settings/integrations/__tests__/oauth-provider-card.test.tsx b/src/components/settings/integrations/__tests__/oauth-provider-card.test.tsx index 9a0f3739a..33ee40a18 100644 --- a/src/components/settings/integrations/__tests__/oauth-provider-card.test.tsx +++ b/src/components/settings/integrations/__tests__/oauth-provider-card.test.tsx @@ -178,3 +178,34 @@ describe("OAuthProviderCard — redirect-URI mini-guide (v1.29.x, UX audit H2)", expect(html).not.toContain('data-testid="polar-redirect-guide"'); }); }); + +/** + * v1.32.28 — sync now. The route without the button would be an endpoint with + * no caller; the button is what makes the manual sync a feature rather than an + * API detail. Rendered only once the provider is connected, in the same action + * row as Test connection. + */ +describe("OAuthProviderCard — sync-now action", () => { + it("offers the sync action on a connected provider", () => { + statusPayload = { + connected: true, + configured: true, + available: true, + state: "connected", + lastSuccessAt: null, + lastError: null, + }; + const html = render(); + expect(html).toContain('data-testid="polar-sync"'); + expect(html).toContain("Sync now"); + }); + + it("does not offer it before the provider is connected", () => { + statusPayload = { + connected: false, + configured: true, + available: true, + }; + expect(render()).not.toContain('data-testid="polar-sync"'); + }); +}); diff --git a/src/components/settings/integrations/nightscout-card.tsx b/src/components/settings/integrations/nightscout-card.tsx index 9392777c1..43ad583b5 100644 --- a/src/components/settings/integrations/nightscout-card.tsx +++ b/src/components/settings/integrations/nightscout-card.tsx @@ -15,6 +15,7 @@ import { Droplet, Link2, Loader2, + RefreshCw, Save, Unlink, } from "lucide-react"; @@ -96,6 +97,11 @@ export function NightscoutCard({ enabled = true }: { enabled?: boolean }) { const [saving, setSaving] = useState(false); const [msg, setMsg] = useState(null); const [msgType, setMsgType] = useState<"success" | "error" | null>(null); + const [syncing, setSyncing] = useState(false); + const [syncMsg, setSyncMsg] = useState(null); + const [syncMsgType, setSyncMsgType] = useState<"success" | "error" | null>( + null, + ); const { data: status } = useQuery({ queryKey: queryKeys.nightscoutStatus(), @@ -170,6 +176,39 @@ export function NightscoutCard({ enabled = true }: { enabled?: boolean }) { setSaving(false); } + // Sync now. Same shape as the WHOOP card's incremental arm, minus the + // full-history dialog — Nightscout has no full-sync arm to call. + async function handleSync() { + setSyncing(true); + setSyncMsg(null); + setSyncMsgType(null); + try { + const res = await apiFetchRaw("/api/nightscout/sync", { method: "POST" }); + const json = await res.json(); + if (res.ok) { + setSyncMsg( + t("settings.nightscoutSyncResult", { count: json.data.imported }), + ); + setSyncMsgType("success"); + void invalidateKeys(queryClient, measurementDependentKeys); + queryClient.invalidateQueries({ + queryKey: queryKeys.nightscoutStatus(), + }); + queryClient.invalidateQueries({ + queryKey: queryKeys.integrationsStatus(), + }); + } else { + setSyncMsg(json.error || t("settings.nightscoutSyncFailed")); + setSyncMsgType("error"); + } + } catch { + setSyncMsg(t("settings.nightscoutSyncFailed")); + setSyncMsgType("error"); + } finally { + setSyncing(false); + } + } + const pillState = pillStateFor(status); const errorMessage = (pillState === "error" || pillState === "parked") && status?.lastError @@ -337,6 +376,22 @@ export function NightscoutCard({ enabled = true }: { enabled?: boolean }) { {status?.connected && ( <>
+
+ {syncMsg && ( +

+ {syncMsg} +

+ )} {/* connect→data loop: a discreet link to where the glucose readings now surface — doubles as the "your data is richer" cue. */} diff --git a/src/components/settings/integrations/oauth-provider-card.tsx b/src/components/settings/integrations/oauth-provider-card.tsx index fe4a05d0d..59c4a9c26 100644 --- a/src/components/settings/integrations/oauth-provider-card.tsx +++ b/src/components/settings/integrations/oauth-provider-card.tsx @@ -15,6 +15,7 @@ import { ArrowRight, Link2, Loader2, + RefreshCw, Save, Unlink, type LucideIcon, @@ -125,6 +126,11 @@ export function OAuthProviderCard({ const { t } = useTranslations(); const queryClient = useQueryClient(); const [msg, setMsg] = useState(null); + const [syncing, setSyncing] = useState(false); + const [syncMsg, setSyncMsg] = useState(null); + const [syncMsgType, setSyncMsgType] = useState<"success" | "error" | null>( + null, + ); const [clientId, setClientId] = useState(""); const [clientSecret, setClientSecret] = useState(""); const [credsSaving, setCredsSaving] = useState(false); @@ -145,6 +151,37 @@ export function OAuthProviderCard({ }); const status = viewModel ?? fetchedStatus; + // Sync now. Same shape as the WHOOP card's incremental arm, minus the + // full-history dialog — none of these providers has a full-sync arm to call. + async function handleSync() { + setSyncing(true); + setSyncMsg(null); + setSyncMsgType(null); + try { + const res = await apiFetchRaw(`/api/${provider}/sync`, { + method: "POST", + }); + const json = await res.json(); + if (res.ok) { + setSyncMsg(t(`${i18nPrefix}SyncResult`, { count: json.data.imported })); + setSyncMsgType("success"); + void invalidateKeys(queryClient, measurementDependentKeys); + queryClient.invalidateQueries({ queryKey: statusQueryKey }); + queryClient.invalidateQueries({ + queryKey: queryKeys.integrationsStatus(), + }); + } else { + setSyncMsg(json.error || t(`${i18nPrefix}SyncFailed`)); + setSyncMsgType("error"); + } + } catch { + setSyncMsg(t(`${i18nPrefix}SyncFailed`)); + setSyncMsgType("error"); + } finally { + setSyncing(false); + } + } + async function handleSaveCredentials(e: React.FormEvent) { e.preventDefault(); setCredsSaving(true); @@ -374,6 +411,22 @@ export function OAuthProviderCard({ {status?.connected ? ( <>
+
+ {syncMsg && ( +

+ {syncMsg} +

+ )} {/* connect→data loop: a discreet link to where this provider's readings now surface — doubles as the "your data is richer" cue. */}