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.
This commit is contained in:
Marc-André Bombeck committed 2026-07-25 04:43:12 +02:00
1 parent a9f1629f82
commit 0bcbb88db1
10 files changed
+252

No files matched your search

+12
View File
@@ -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.",
+12
View File
@@ -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.",
+12
View File
@@ -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.",
+12
View File
@@ -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.",
+12
View File
@@ -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.",
+12
View File
@@ -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.",
@@ -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"');
});
});
@@ -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"');
});
});
@@ -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<string | null>(null);
const [msgType, setMsgType] = useState<"success" | "error" | null>(null);
const [syncing, setSyncing] = useState(false);
const [syncMsg, setSyncMsg] = useState<string | null>(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 && (
<>
<div className="flex flex-wrap items-start gap-2 [&>*]:min-w-[10rem] sm:[&>*]:min-w-0">
<Button
type="button"
variant="outline"
size="sm"
className="min-h-11"
onClick={handleSync}
disabled={syncing}
data-testid="nightscout-sync"
>
{syncing ? (
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<RefreshCw className="h-3.5 w-3.5" />
)}
{t("settings.nightscoutSync")}
</Button>
<TestConnectionButton
endpoint="/api/nightscout/test"
disabled={!status?.connected}
@@ -373,6 +428,15 @@ export function NightscoutCard({ enabled = true }: { enabled?: boolean }) {
</AlertDialogContent>
</AlertDialog>
</div>
{syncMsg && (
<p
role="alert"
data-testid="nightscout-sync-result"
className={`text-sm ${syncMsgType === "success" ? "text-success" : "text-destructive"}`}
>
{syncMsg}
</p>
)}
{/* connect→data loop: a discreet link to where the glucose
readings now surface — doubles as the "your data is richer"
cue. */}
@@ -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<string | null>(null);
const [syncing, setSyncing] = useState(false);
const [syncMsg, setSyncMsg] = useState<string | null>(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 ? (
<>
<div className="flex flex-wrap items-start gap-2 [&>*]:min-w-[10rem] sm:[&>*]:min-w-0">
<Button
type="button"
variant="outline"
size="sm"
className="min-h-11"
onClick={handleSync}
disabled={syncing}
data-testid={`${provider}-sync`}
>
{syncing ? (
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<RefreshCw className="h-3.5 w-3.5" />
)}
{t(`${i18nPrefix}Sync`)}
</Button>
<TestConnectionButton
endpoint={`/api/${provider}/test`}
disabled={!status?.connected}
@@ -411,6 +464,15 @@ export function OAuthProviderCard({
</AlertDialogContent>
</AlertDialog>
</div>
{syncMsg && (
<p
role="alert"
data-testid={`${provider}-sync-result`}
className={`text-sm ${syncMsgType === "success" ? "text-success" : "text-destructive"}`}
>
{syncMsg}
</p>
)}
{/* connect→data loop: a discreet link to where this provider's
readings now surface — doubles as the "your data is richer"
cue. */}