feat(integrations): say where an imported Google Health history ends

The two status routes publish backfillEnded: each data type whose
backfill stopped short of the start of its history, with the instant it
reaches back to and whether the horizon or a walk that stopped moving
ended it. The integration card lists them in place of an import that
would otherwise read as still loading, in all seven locales.

Refs #1220
This commit is contained in:
Marc-André Bombeck committed 2026-10-19 17:25:57 +02:00
1 parent 72c259d85c
commit f6b09f07cd
15 files changed
+213

No files matched your search

@@ -137,6 +137,35 @@ describe("<IntegrationCard> per state", () => {
);
});
it("history ended short: no longer loading, and says where it ends and why", () => {
const html = render(
"google-health",
connected(healthFixture(), {
backfillCompleted: true,
backfillEnded: [
{
type: "PULSE",
endsAt: "2024-05-11T08:00:00.000Z",
reason: "no_progress",
},
{
type: "WEIGHT",
endsAt: "2006-10-17T12:00:00.000Z",
reason: "horizon",
},
],
}),
);
expect(html).not.toContain("Older history is loading");
expect(html).toContain('data-testid="google-health-history-ends"');
expect(html).toMatch(
/data-reason="no_progress"[^>]*>Pulse: imported back to [^<]*2024[^<]*\. Google stopped returning older readings/,
);
expect(html).toMatch(
/data-reason="horizon"[^>]*>Weight: imported back to [^<]*2006[^<]*\. Older readings lie beyond the import limit\./,
);
});
it("limited by plan: the sentence names up to three, counts the rest, no button", () => {
const html = render(
"withings",
@@ -0,0 +1,46 @@
"use client";
/**
* Where an integration's imported history ends, when its backfill stopped
* before the beginning of a data type's history (#1220).
*
* Without it a backfill that could not go further either kept the card
* saying older history was loading, forever, or (once it completes) would
* leave a gap nobody explained. The server resolves where each type ends and
* why; this only words it. Each line is two sentences about the user's own
* data, so it reads as body text (`text-sm`, foreground), not as muted meta.
*/
import { MEASUREMENT_TYPE_LABEL_KEYS } from "@/components/measurements/measurement-list-meta";
import { useFormatters, useTranslations } from "@/lib/i18n/context";
import type { IntegrationStatusViewModel } from "./shared";
export function HistoryEndsNote({
provider,
entries,
}: {
provider: string;
entries: IntegrationStatusViewModel["backfillEnded"];
}) {
const { t } = useTranslations();
const fmt = useFormatters();
if (!entries?.length) return null;
return (
<ul className="space-y-1 text-sm" data-testid={`${provider}-history-ends`}>
{entries.map((entry) => {
const labelKey = MEASUREMENT_TYPE_LABEL_KEYS[entry.type];
const values = {
type: labelKey ? t(labelKey) : entry.type,
date: fmt.date(entry.endsAt),
};
return (
<li key={entry.type} data-reason={entry.reason}>
{entry.reason === "horizon"
? t("settings.integrationCard.historyEnds.horizon", values)
: t("settings.integrationCard.historyEnds.noProgress", values)}
</li>
);
})}
</ul>
);
}
@@ -55,6 +55,7 @@ import {
IntegrationCard,
type IntegrationMenuAction,
} from "./integration-card";
import { HistoryEndsNote } from "./history-ends-note";
import { MetricFreshnessList } from "./metric-freshness-list";
import {
CallbackMismatchNotice,
@@ -505,6 +506,7 @@ export function OAuthIntegrationCardView({
) : undefined
}
>
<HistoryEndsNote provider={provider} entries={viewModel?.backfillEnded} />
{syncResults ?? (
<ActionResultLines provider={provider} sync={sync} test={test} />
)}
@@ -54,6 +54,15 @@ export interface IntegrationStatusViewModel {
connected?: boolean;
/** WHOOP / Fitbit / Google Health: older history still loading. */
backfillCompleted?: boolean | null;
/**
* Google Health: data types whose history backfill ended before the start
* of their history, with where and why.
*/
backfillEnded?: Array<{
type: string;
endsAt: string;
reason: "horizon" | "no_progress";
}>;
/**
* Polar / Oura / Strava: usable credentials resolve (BYO or the shared
* app). Greys out "Connect" when false.