diff --git a/messages/de.json b/messages/de.json
index a583ed16d..bc7ac86ec 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -6674,21 +6674,12 @@
}
},
"integrationPill": {
- "connected": "Verbunden",
- "errorReconnect": "Fehler: neu verbinden",
- "warningServerError": "Sync fehlgeschlagen",
- "failureCount": "{count}/{threshold}",
- "parkedReconnect": "Pausiert: neu verbinden",
- "notConnected": "Nicht verbunden",
"justNow": "gerade eben",
"minutesAgo": "vor {count} min",
"hoursAgo": "vor {count} Std.",
"daysAgo": "vor {count} d",
"resumeSuccess": "Integration wieder aktiv",
- "resumeError": "Wieder verbinden fehlgeschlagen",
- "stale": "Verbunden, keine neuen Daten",
- "stalled": "Sync gestoppt",
- "pendingSetup": "Warte auf erste Daten"
+ "resumeError": "Wieder verbinden fehlgeschlagen"
},
"integrationFreshness": {
"title": "Empfangene Daten",
diff --git a/messages/en.json b/messages/en.json
index 35ae9c711..780787051 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -6674,21 +6674,12 @@
}
},
"integrationPill": {
- "connected": "Connected",
- "errorReconnect": "Error: reconnect",
- "warningServerError": "Sync failing",
- "failureCount": "{count}/{threshold}",
- "parkedReconnect": "Paused: reconnect",
- "notConnected": "Not connected",
"justNow": "just now",
"minutesAgo": "{count} min ago",
"hoursAgo": "{count} h ago",
"daysAgo": "{count} d ago",
"resumeSuccess": "Integration resumed",
- "resumeError": "Reconnect failed",
- "stale": "Connected, no recent data",
- "stalled": "Sync stopped",
- "pendingSetup": "Waiting for first data"
+ "resumeError": "Reconnect failed"
},
"integrationFreshness": {
"title": "Data received",
diff --git a/messages/es.json b/messages/es.json
index 2cfd320aa..8782a47a7 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -6674,21 +6674,12 @@
}
},
"integrationPill": {
- "connected": "Conectado",
- "errorReconnect": "Error: reconectar",
- "warningServerError": "Sincronización con errores",
- "failureCount": "{count}/{threshold}",
- "parkedReconnect": "Pausado: reconectar",
- "notConnected": "No conectado",
"justNow": "ahora mismo",
"minutesAgo": "hace {count} min",
"hoursAgo": "hace {count} h",
"daysAgo": "hace {count} d",
"resumeSuccess": "Integración reanudada",
- "resumeError": "Reconexión fallida",
- "stale": "Conectado, sin datos recientes",
- "stalled": "Sincronización detenida",
- "pendingSetup": "Esperando los primeros datos"
+ "resumeError": "Reconexión fallida"
},
"integrationFreshness": {
"title": "Datos recibidos",
diff --git a/messages/fr.json b/messages/fr.json
index 1370a2b4e..f2db6c1a6 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -6674,21 +6674,12 @@
}
},
"integrationPill": {
- "connected": "Connecté",
- "errorReconnect": "Erreur : reconnecter",
- "warningServerError": "Échec de synchronisation",
- "failureCount": "{count}/{threshold}",
- "parkedReconnect": "En pause : reconnecter",
- "notConnected": "Non connecté",
"justNow": "à l’instant",
"minutesAgo": "il y a {count} min",
"hoursAgo": "il y a {count} h",
"daysAgo": "il y a {count} j",
"resumeSuccess": "Intégration reprise",
- "resumeError": "Échec de la reconnexion",
- "stale": "Connecté, aucune donnée récente",
- "stalled": "Synchronisation arrêtée",
- "pendingSetup": "En attente des premières données"
+ "resumeError": "Échec de la reconnexion"
},
"integrationFreshness": {
"title": "Données reçues",
diff --git a/messages/it.json b/messages/it.json
index 6d1583fda..ea19c378e 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -6674,21 +6674,12 @@
}
},
"integrationPill": {
- "connected": "Connesso",
- "errorReconnect": "Errore, riconnetti",
- "warningServerError": "Sincronizzazione in errore",
- "failureCount": "{count}/{threshold}",
- "parkedReconnect": "In pausa, riconnetti",
- "notConnected": "Non connesso",
"justNow": "proprio ora",
"minutesAgo": "{count} min fa",
"hoursAgo": "{count} h fa",
"daysAgo": "{count} g fa",
"resumeSuccess": "Integrazione ripresa",
- "resumeError": "Riconnessione fallita",
- "stale": "Connesso, nessun dato recente",
- "stalled": "Sincronizzazione ferma",
- "pendingSetup": "In attesa dei primi dati"
+ "resumeError": "Riconnessione fallita"
},
"integrationFreshness": {
"title": "Dati ricevuti",
diff --git a/messages/ko.json b/messages/ko.json
index c98d10cf8..3a753a8f9 100644
--- a/messages/ko.json
+++ b/messages/ko.json
@@ -6674,21 +6674,12 @@
}
},
"integrationPill": {
- "connected": "연결됨",
- "errorReconnect": "오류: 다시 연결",
- "warningServerError": "동기화 실패 중",
- "failureCount": "{count}/{threshold}",
- "parkedReconnect": "일시중지: 다시 연결",
- "notConnected": "연결 안 됨",
"justNow": "방금",
"minutesAgo": "{count}분 전",
"hoursAgo": "{count}시간 전",
"daysAgo": "{count}일 전",
"resumeSuccess": "연동을 다시 시작했어요",
- "resumeError": "다시 연결하지 못했어요",
- "stale": "연결됨, 최근 데이터 없음",
- "stalled": "동기화 멈춤",
- "pendingSetup": "첫 데이터 기다리는 중"
+ "resumeError": "다시 연결하지 못했어요"
},
"integrationFreshness": {
"title": "받은 데이터",
diff --git a/messages/nl.json b/messages/nl.json
index c51305972..3b80f1854 100644
--- a/messages/nl.json
+++ b/messages/nl.json
@@ -6674,21 +6674,12 @@
}
},
"integrationPill": {
- "connected": "Gekoppeld",
- "errorReconnect": "Fout: opnieuw koppelen",
- "warningServerError": "Synchronisatie mislukt",
- "failureCount": "{count}/{threshold}",
- "parkedReconnect": "Gepauzeerd: opnieuw koppelen",
- "notConnected": "Niet gekoppeld",
"justNow": "zojuist",
"minutesAgo": "{count} min geleden",
"hoursAgo": "{count} u geleden",
"daysAgo": "{count} d geleden",
"resumeSuccess": "Koppeling hervat",
- "resumeError": "Opnieuw koppelen mislukt",
- "stale": "Gekoppeld, geen recente gegevens",
- "stalled": "Synchronisatie gestopt",
- "pendingSetup": "Wachten op eerste gegevens"
+ "resumeError": "Opnieuw koppelen mislukt"
},
"integrationFreshness": {
"title": "Ontvangen gegevens",
diff --git a/messages/pl.json b/messages/pl.json
index 06a06f600..78b02dc65 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -6674,21 +6674,12 @@
}
},
"integrationPill": {
- "connected": "Połączono",
- "errorReconnect": "Błąd: połącz ponownie",
- "warningServerError": "Synchronizacja nie działa",
- "failureCount": "{count}/{threshold}",
- "parkedReconnect": "Wstrzymane: połącz ponownie",
- "notConnected": "Nie połączono",
"justNow": "przed chwilą",
"minutesAgo": "{count} min temu",
"hoursAgo": "{count} godz. temu",
"daysAgo": "{count} dni temu",
"resumeSuccess": "Integracja wznowiona",
- "resumeError": "Ponowne połączenie nieudane",
- "stale": "Połączono, brak nowych danych",
- "stalled": "Synchronizacja zatrzymana",
- "pendingSetup": "Czekam na pierwsze dane"
+ "resumeError": "Ponowne połączenie nieudane"
},
"integrationFreshness": {
"title": "Odebrane dane",
diff --git a/src/__tests__/success-affordance-guard.test.ts b/src/__tests__/success-affordance-guard.test.ts
index 0677418be..a41563ed7 100644
--- a/src/__tests__/success-affordance-guard.test.ts
+++ b/src/__tests__/success-affordance-guard.test.ts
@@ -353,10 +353,6 @@ const PINNED_AFFORDANCES: Record<
"src/components/settings/insights-pill-order-section.tsx": {
"toast.success": 1,
},
- "src/components/settings/integration-status-pill.tsx": {
- CheckCircle2: 2,
- "text-success": 1,
- },
"src/components/settings/integrations/connections-panel.tsx": {
"toast.success": 2,
},
diff --git a/src/components/settings/__tests__/integration-status-pill.test.tsx b/src/components/settings/__tests__/integration-status-pill.test.tsx
deleted file mode 100644
index d1663c12d..000000000
--- a/src/components/settings/__tests__/integration-status-pill.test.tsx
+++ /dev/null
@@ -1,286 +0,0 @@
-/**
- * v1.4.19 — IntegrationStatusPill
- *
- * The single tag rendered top-right of every integration card. The
- * pill is the ONLY place a connection status surfaces — the maintainer was
- * staring at three- to four-fold redundancy across Withings + Mood Log
- * cards in v1.4.18 and called it out. This component is reusable so
- * v1.4.20 can drop the same pill on the Apple Health card.
- *
- * The four states this file locks in (one assertion each):
- * 1. connected → "Connected · 12 min ago" pattern
- * 2. error → "Error — reconnect" pattern
- * 3. error (without ts) → bare error label, no "ago" suffix
- * 4. disconnected → "Not connected" pattern (no relative ts)
- *
- * Plus locale parity: switching to `de` renders the German strings.
- * Plus a "no last-sync timestamp" guard for connected state.
- */
-import { describe, expect, it } from "vitest";
-import { renderToStaticMarkup } from "react-dom/server";
-
-import { I18nProvider } from "@/lib/i18n/context";
-import { IntegrationStatusPill } from "../integration-status-pill";
-import { primeLocales } from "@/__tests__/helpers/prime-locales";
-
-primeLocales("de");
-
-function render(node: React.ReactNode, locale: "en" | "de" = "en") {
- return renderToStaticMarkup(
- {node},
- );
-}
-
-describe("IntegrationStatusPill", () => {
- it("renders the connected state with relative time when lastSyncAt is recent", () => {
- const twelveMinAgo = new Date(Date.now() - 12 * 60 * 1000);
- const html = render(
- ,
- );
- expect(html).toContain("Connected");
- // 12 min in EN locale
- expect(html).toMatch(/12\s+min(\.|utes)?\s+ago/);
- // Renders within a single pill marker so cards can target it.
- expect(html).toContain('data-testid="integration-status-pill"');
- expect(html).toContain('data-state="connected"');
- });
-
- it("renders the connected state without relative time when lastSyncAt is null", () => {
- const html = render(
- ,
- );
- expect(html).toContain("Connected");
- // Must not render the relative-time separator " · " when the
- // timestamp is missing, otherwise the pill reads "Connected · ".
- expect(html).not.toMatch(/Connected.*\s·\s\s/);
- });
-
- it("renders the error state with the reconnect-cta phrasing", () => {
- const html = render(
- ,
- );
- expect(html).toContain("Error");
- expect(html).toContain("reconnect");
- expect(html).toContain('data-state="error"');
- });
-
- it("renders the disconnected state with the 'not connected' label", () => {
- const html = render(
- ,
- );
- expect(html).toContain("Not connected");
- expect(html).toContain('data-state="disconnected"');
- });
-
- it("renders German strings when the active locale is 'de'", () => {
- const html = render(
- ,
- "de",
- );
- expect(html).toContain("Nicht verbunden");
- });
-
- it("uses an abbreviated relative-time form for very recent syncs (< 1 min)", () => {
- const fortySecondsAgo = new Date(Date.now() - 40 * 1000);
- const html = render(
- ,
- );
- // Sub-minute syncs collapse to "just now" so the pill stays
- // narrow on Pixel 5.
- expect(html).toMatch(/just\s+now/i);
- });
-
- // v1.4.43 — parked-state copy.
- // Distinct from `error` (red reconnect pill) because the user can
- // resume the integration without redoing the OAuth dance; distinct
- // from `warning` because the persistent streak survived the alert
- // ladder AND the 24h grace window — manual intervention required.
- it("renders the parked state with manual-reconnect phrasing (EN)", () => {
- const html = render(
- ,
- );
- expect(html).toContain("Paused");
- expect(html).toContain("Paused: reconnect");
- expect(html).toContain('data-state="parked"');
- });
-
- it("renders the parked state with manual-reconnect phrasing (DE)", () => {
- const html = render(
- ,
- "de",
- );
- expect(html).toContain("Pausiert");
- expect(html).toContain("Pausiert: neu verbinden");
- });
-
- // The three states the server verdict added. `stale` and `stalled` both
- // carry an inline timestamp, because "since when" is the whole point of
- // them; `pending-setup` has nothing to timestamp yet.
- it("renders the stale state in the warning tone with its timestamp", () => {
- const html = render(
- ,
- );
- expect(html).toContain('data-state="stale"');
- expect(html).toContain("no recent data");
- expect(html).toMatch(/9\s+d\s+ago/);
- expect(html).toContain("bg-warning/15");
- });
-
- it("renders the stalled state as a stopped sync, not an error", () => {
- const html = render(
- ,
- );
- expect(html).toContain('data-state="stalled"');
- expect(html).toContain("Sync stopped");
- expect(html).toMatch(/28\s+d\s+ago/);
- // Warning tone, never destructive: the user cannot fix a cron that
- // stopped by clicking reconnect.
- expect(html).toContain("bg-warning/15");
- expect(html).not.toContain('data-variant="destructive"');
- });
-
- it("renders the pending-setup state with no timestamp", () => {
- const html = render(
- ,
- );
- expect(html).toContain('data-state="pending-setup"');
- expect(html).toContain("Waiting for first data");
- expect(html).not.toContain("bg-warning/15");
- });
-
- /**
- * Red is reserved for "your action fixes this". A transient upstream failure
- * is not that: telling the user to reconnect against a 503 sends them
- * clicking at something they cannot repair.
- */
- it("keeps the destructive variant for reauth alone", () => {
- expect(
- render(),
- ).toContain('data-variant="destructive"');
- for (const state of [
- "warning",
- "stale",
- "stalled",
- "parked",
- "pending-setup",
- "connected",
- ] as const) {
- expect(
- render(),
- state,
- ).not.toContain('data-variant="destructive"');
- }
- });
-
- it("lets a card keep its own established testid", () => {
- const html = render(
- ,
- );
- expect(html).toContain('data-testid="apple-health-status"');
- });
-});
-
-describe("IntegrationStatusPill — a failing provider states its age", () => {
- it("renders the failing state with how long it has been failing", () => {
- const seventeenDaysAgo = new Date(Date.now() - 17 * 24 * 60 * 60 * 1000);
- const html = render(
- ,
- );
- // The label must not open with a claim of connection: the pipe has
- // delivered nothing for a fortnight.
- expect(html).not.toContain("Connected");
- expect(html).toContain("Sync failing");
- // And the age has to be on the pill. Without it, hour one and day
- // seventeen render identically, which is how a dead sync stayed quiet.
- expect(html).toMatch(/17\s*d\s+ago/);
- expect(html).toContain('data-state="warning"');
- });
-
- it("renders the localized failure count against the server threshold", () => {
- const html = render(
- ,
- );
-
- expect(html).toContain("2/3");
- });
-
- it("omits a partial failure ratio", () => {
- const html = render(
- ,
- );
-
- expect(html).not.toContain("2/");
- });
-
- it("keeps the German label free of a connection claim too", () => {
- const html = render(
- ,
- "de",
- );
- expect(html).not.toContain("Verbunden");
- expect(html).toContain("Sync fehlgeschlagen");
- });
-});
-
-describe("IntegrationStatusPill — accessible name", () => {
- /**
- * A generic `aria-label` used to sit on the badge. Being a label, it
- * REPLACED the accessible name with itself, so a screen reader announced the
- * same words for a healthy connection as for a broken one — erasing the one
- * distinction the pill exists to draw.
- */
- it.each([
- ["connected", "Connected"],
- ["error", "Error"],
- ["parked", "Paused"],
- ] as const)("lets the %s label be the accessible name", (state, label) => {
- const html = render(
- ,
- );
- expect(html).not.toContain("aria-label=");
- expect(html).toContain(label);
- });
-});
diff --git a/src/components/settings/_card-header.tsx b/src/components/settings/_card-header.tsx
index 16f53a465..d0382bd69 100644
--- a/src/components/settings/_card-header.tsx
+++ b/src/components/settings/_card-header.tsx
@@ -61,8 +61,7 @@ export interface SettingsCardHeaderProps {
* anchors it relies on are pinned in `src/lib/command-palette/
* settings-cards.ts`. */
anchor?: string;
- /** Optional right-aligned status surface — typically an
- * `` or a wrapper around badges. */
+ /** Optional right-aligned status surface — typically a badge. */
status?: React.ReactNode;
/** Optional extra classes applied to the outer wrapper. */
className?: string;
diff --git a/src/components/settings/integration-status-pill.tsx b/src/components/settings/integration-status-pill.tsx
deleted file mode 100644
index 50e87119e..000000000
--- a/src/components/settings/integration-status-pill.tsx
+++ /dev/null
@@ -1,242 +0,0 @@
-"use client";
-
-import {
- CheckCircle2,
- AlertTriangle,
- CircleSlash,
- Clock,
- Info,
- PauseCircle,
-} from "lucide-react";
-
-import { Badge } from "@/components/ui/badge";
-import { useTranslations } from "@/lib/i18n/context";
-import { cn } from "@/lib/utils";
-
-/**
- * v1.4.19 — single status pill rendered top-right of every
- * integration card (Withings, Mood Log, Apple Health…). It replaces
- * the three- to four-fold redundant status surface the maintainer spotted in
- * v1.4.18:
- *
- * - top-right "Connected" badge
- * - middle container with "Connected / last successful / last attempt"
- * - bottom-of-card "letzter Sync" line
- *
- * Now there is exactly ONE place a sync status can appear on a card
- * and it follows the pattern the maintainer liked from the Withings header: a
- * Dracula-tokenized chip with the relative time inline.
- *
- * The component is locale-aware (EN + DE) and mobile-safe: on a
- * Pixel-5 viewport (393 CSS px) the pill stays on the same line as
- * the card title because it whitespace-nowraps and tucks the icon +
- * abbreviated time form into a 12 char string at most.
- */
-
-/**
- * Pill states — a thin projection of the server's sync-health verdict.
- *
- * - connected → happy path; relative "last sync" suffix is shown.
- * - stale → connected, but no new data for a while; suffix shown.
- * - stalled → the sync stopped even attempting; suffix shown.
- * - warning → attempting and failing (a 5xx, a rate limit, an upstream
- * contract mismatch). Not the user's to fix.
- * - error → reauth-required; "Error — reconnect" copy.
- * - parked → >24h of persistent failures put the integration to
- * sleep; "Paused — reconnect manually" copy + resume CTA.
- * - pending-setup → configured but nothing has arrived yet.
- * - disconnected → user clicked Disconnect; "Not connected" copy.
- *
- * **Red is reserved for "your action fixes this".** A transient upstream
- * failure carries the warning tone, not the destructive one: telling a user to
- * reconnect cannot help against a 503, and ten fruitless reconnect clicks are
- * the cost of saying otherwise.
- */
-export type IntegrationPillState =
- | "connected"
- | "stale"
- | "stalled"
- | "warning"
- | "error"
- | "parked"
- | "pending-setup"
- | "disconnected";
-
-interface IntegrationStatusPillProps {
- state: IntegrationPillState;
- /** When connected, the last-successful-sync timestamp drives the
- * relative "12 min ago" suffix. Pass `null` to suppress the
- * suffix (used when the integration was just configured but has
- * never synced yet). */
- lastSyncAt: Date | string | null;
- /** Current consecutive-failure count from the active server bucket. */
- failureCount?: number;
- /** Server-owned alert threshold paired with `failureCount`. */
- failureThreshold?: number;
- /** Override "now" for deterministic testing. Defaults to `new Date()`. */
- now?: Date;
- /** Override the testid so a card can keep its own established e2e hook. */
- testId?: string;
- className?: string;
-}
-
-function toDate(value: Date | string): Date {
- return value instanceof Date ? value : new Date(value);
-}
-
-/**
- * Translate a millisecond delta into a short, locale-aware relative
- * string. We keep the buckets small and fixed (just-now / minutes /
- * hours / days) because the pill must fit on a 360 px wide card.
- */
-export function formatRelative(
- deltaMs: number,
- t: (key: string, params?: Record) => string,
-): string {
- if (deltaMs < 60_000) return t("settings.integrationPill.justNow");
- const minutes = Math.floor(deltaMs / 60_000);
- if (minutes < 60) {
- return t("settings.integrationPill.minutesAgo", { count: minutes });
- }
- const hours = Math.floor(minutes / 60);
- if (hours < 24) {
- return t("settings.integrationPill.hoursAgo", { count: hours });
- }
- const days = Math.floor(hours / 24);
- return t("settings.integrationPill.daysAgo", { count: days });
-}
-
-/** The states that paint the semantic warning chip rather than a variant. */
-const WARNING_STATES = new Set([
- "stale",
- "stalled",
- "warning",
- "parked",
-]);
-
-/**
- * The states whose label carries an inline relative timestamp.
- *
- * `warning` is in the set because leaving it out was the whole lie: the pill
- * said "sync issue" identically on hour one and on day seventeen, and the age
- * — which the mapper computes and passes in — was dropped on the floor. How
- * long it has been broken is the only part of that sentence a person can act
- * on.
- */
-const RELATIVE_STATES = new Set([
- "connected",
- "stale",
- "stalled",
- "warning",
-]);
-
-export function IntegrationStatusPill({
- state,
- lastSyncAt,
- failureCount,
- failureThreshold,
- now,
- testId,
- className,
-}: IntegrationStatusPillProps) {
- const { t } = useTranslations();
-
- // The chip classes apply to `connected` and to every warning-tone state —
- // `error` falls back to `variant="destructive"`, `disconnected` and
- // `pending-setup` to `variant="outline"`. The warning tone is an AA-safe
- // amber that clears contrast in light mode; the distinct labels + icons
- // carry which flavour of "not right" this is.
- const connectedChipClass = "border-success/30 bg-success/15 text-success";
- const warningChipClass = "border-warning/30 bg-warning/15 text-warning";
-
- let label: string;
- let icon: React.ReactNode;
-
- switch (state) {
- case "connected":
- label = t("settings.integrationPill.connected");
- icon = ;
- break;
- case "stale":
- label = t("settings.integrationPill.stale");
- icon = ;
- break;
- case "stalled":
- label = t("settings.integrationPill.stalled");
- icon = ;
- break;
- case "warning":
- label = t("settings.integrationPill.warningServerError");
- icon = ;
- break;
- case "error":
- label = t("settings.integrationPill.errorReconnect");
- icon = ;
- break;
- case "parked":
- label = t("settings.integrationPill.parkedReconnect");
- icon = ;
- break;
- case "pending-setup":
- label = t("settings.integrationPill.pendingSetup");
- icon = ;
- break;
- case "disconnected":
- label = t("settings.integrationPill.notConnected");
- icon = ;
- break;
- }
-
- const reference = now ?? new Date();
- const relative =
- RELATIVE_STATES.has(state) && lastSyncAt
- ? formatRelative(
- Math.max(0, reference.getTime() - toDate(lastSyncAt).getTime()),
- t,
- )
- : null;
- const failureRatio =
- failureCount !== undefined &&
- failureCount > 0 &&
- failureThreshold !== undefined
- ? t("settings.integrationPill.failureCount", {
- count: failureCount,
- threshold: failureThreshold,
- })
- : null;
-
- return (
-
- {icon}
-
- {label}
- {failureRatio ? ` · ${failureRatio}` : null}
- {relative ? · {relative} : null}
-
- {relative ? {relative} : null}
-
- );
-}
diff --git a/src/components/settings/integrations/apple-health-card.tsx b/src/components/settings/integrations/apple-health-card.tsx
index 4ba5a3912..4b6eb0bcd 100644
--- a/src/components/settings/integrations/apple-health-card.tsx
+++ b/src/components/settings/integrations/apple-health-card.tsx
@@ -14,7 +14,7 @@ import Link from "next/link";
import { useQuery } from "@tanstack/react-query";
import { HeartPulse, Upload } from "lucide-react";
-import { formatRelative } from "@/components/settings/integration-status-pill";
+import { formatRelative } from "./format-relative";
import { Button } from "@/components/ui/button";
import { QueryErrorRow } from "@/components/ui/query-error-row";
import { apiGet } from "@/lib/api/api-fetch";
diff --git a/src/components/settings/integrations/format-relative.ts b/src/components/settings/integrations/format-relative.ts
new file mode 100644
index 000000000..ed2dcca1d
--- /dev/null
+++ b/src/components/settings/integrations/format-relative.ts
@@ -0,0 +1,21 @@
+/**
+ * "just now" / "12 min ago" / "3 h ago" / "2 days ago" for a sync or arrival
+ * age in milliseconds. Used by the Apple Health card and the per-metric
+ * freshness list; it outlived the status pill it was written for.
+ */
+export function formatRelative(
+ deltaMs: number,
+ t: (key: string, params?: Record) => string,
+): string {
+ if (deltaMs < 60_000) return t("settings.integrationPill.justNow");
+ const minutes = Math.floor(deltaMs / 60_000);
+ if (minutes < 60) {
+ return t("settings.integrationPill.minutesAgo", { count: minutes });
+ }
+ const hours = Math.floor(minutes / 60);
+ if (hours < 24) {
+ return t("settings.integrationPill.hoursAgo", { count: hours });
+ }
+ const days = Math.floor(hours / 24);
+ return t("settings.integrationPill.daysAgo", { count: days });
+}
diff --git a/src/components/settings/integrations/metric-freshness-list.tsx b/src/components/settings/integrations/metric-freshness-list.tsx
index 6fa69b335..6fe93c55d 100644
--- a/src/components/settings/integrations/metric-freshness-list.tsx
+++ b/src/components/settings/integrations/metric-freshness-list.tsx
@@ -22,7 +22,7 @@
import type { ReactNode } from "react";
import { AlertTriangle } from "lucide-react";
-import { formatRelative } from "@/components/settings/integration-status-pill";
+import { formatRelative } from "./format-relative";
import { MEASUREMENT_TYPE_LABEL_KEYS } from "@/components/measurements/measurement-list-meta";
import { useTranslations } from "@/lib/i18n/context";
import type { MetricFreshnessEntry } from "@/lib/integrations/sync-verdict";