diff --git a/messages/de.json b/messages/de.json
index 52dff214e..61931321d 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -302,6 +302,7 @@
"sourceManual": "Manuell",
"sourceWithings": "Withings",
"sourceImport": "Import",
+ "sourceAppleHealth": "Apple Health",
"systolicLabel": "Systolisch (mmHg)",
"diastolicLabel": "Diastolisch (mmHg)",
"pulseLabel": "Puls (bpm)",
diff --git a/messages/en.json b/messages/en.json
index 9ec1a84bd..85c0ac4c8 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -302,6 +302,7 @@
"sourceManual": "Manual",
"sourceWithings": "Withings",
"sourceImport": "Import",
+ "sourceAppleHealth": "Apple Health",
"systolicLabel": "Systolic (mmHg)",
"diastolicLabel": "Diastolic (mmHg)",
"pulseLabel": "Pulse (bpm)",
diff --git a/src/app/api/admin/backups/[id]/restore/route.ts b/src/app/api/admin/backups/[id]/restore/route.ts
index f95466b2c..384df52d6 100644
--- a/src/app/api/admin/backups/[id]/restore/route.ts
+++ b/src/app/api/admin/backups/[id]/restore/route.ts
@@ -80,9 +80,22 @@ const MEASUREMENT_TYPES = new Set([
"TOTAL_BODY_WATER",
"BONE_MASS",
"OXYGEN_SATURATION",
+ // ── v1.4.23 Apple Health additions ──
+ "HEART_RATE_VARIABILITY",
+ "RESTING_HEART_RATE",
+ "ACTIVE_ENERGY_BURNED",
+ "FLIGHTS_CLIMBED",
+ "WALKING_RUNNING_DISTANCE",
+ "VO2_MAX",
+ "BODY_TEMPERATURE",
]);
-const MEASUREMENT_SOURCES = new Set(["MANUAL", "WITHINGS", "IMPORT"]);
+const MEASUREMENT_SOURCES = new Set([
+ "MANUAL",
+ "WITHINGS",
+ "IMPORT",
+ "APPLE_HEALTH",
+]);
const INTAKE_SOURCES = new Set(["WEB", "API", "REMINDER", "IMPORT"]);
const handler = apiHandler(
diff --git a/src/components/measurements/__tests__/measurement-list-apple-health-badge.test.tsx b/src/components/measurements/__tests__/measurement-list-apple-health-badge.test.tsx
new file mode 100644
index 000000000..bb896d084
--- /dev/null
+++ b/src/components/measurements/__tests__/measurement-list-apple-health-badge.test.tsx
@@ -0,0 +1,88 @@
+import { describe, expect, it, vi } from "vitest";
+import { renderToStaticMarkup } from "react-dom/server";
+
+/**
+ * v1.4.23 — Apple Health source badge regression guard.
+ *
+ * Mirrors the BP-badge guard above: any non-MANUAL source must paint a
+ * Badge with the localised source label. APPLE_HEALTH gets the
+ * Dracula-pink chip; the helper falls back to the SCREAMING_SNAKE
+ * value if it's an unknown source so this test would catch a missing
+ * `sourceAppleHealth` translation key as a stray `APPLE_HEALTH`
+ * substring leaking into the DOM.
+ */
+
+const baseMeasurements = [
+ {
+ id: "m-apple-1",
+ type: "WEIGHT",
+ value: 81.5,
+ unit: "kg",
+ source: "APPLE_HEALTH",
+ measuredAt: "2026-05-09T08:30:00.000Z",
+ notes: null,
+ },
+];
+
+vi.mock("next/navigation", () => ({
+ useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
+ usePathname: () => "/measurements",
+}));
+
+vi.mock("@tanstack/react-query", () => ({
+ useQuery: () => ({
+ data: { measurements: baseMeasurements, meta: { total: 1 } },
+ isLoading: false,
+ }),
+ useQueryClient: () => ({ invalidateQueries: vi.fn() }),
+ useMutation: () => ({
+ mutate: vi.fn(),
+ mutateAsync: vi.fn(),
+ isPending: false,
+ isError: false,
+ error: null,
+ }),
+}));
+
+vi.mock("@/hooks/use-auth", () => ({
+ useAuth: () => ({
+ user: { id: "u1", username: "marc", role: "USER" },
+ isAuthenticated: true,
+ isLoading: false,
+ }),
+}));
+
+import { I18nProvider } from "@/lib/i18n/context";
+import { MeasurementList } from "../measurement-list";
+
+function render(locale: "en" | "de" = "en") {
+ return renderToStaticMarkup(
+
+
+ ,
+ );
+}
+
+describe("MeasurementList — APPLE_HEALTH badge", () => {
+ it("renders the localised Apple Health label, never the raw enum", () => {
+ const html = render("en");
+ expect(html).toContain("Apple Health");
+ expect(html).not.toContain(">APPLE_HEALTH<");
+ });
+
+ it("paints the badge with the Dracula-pink utility classes", () => {
+ const html = render("en");
+ // The chip class composition (`border-dracula-pink/50`,
+ // `bg-dracula-pink/15`, `text-dracula-pink`) is what gives the
+ // badge its iOS-accent identity. Asserting all three keeps the
+ // colour contract from drifting silently.
+ expect(html).toMatch(/border-dracula-pink\/50/);
+ expect(html).toMatch(/bg-dracula-pink\/15/);
+ expect(html).toMatch(/text-dracula-pink/);
+ });
+
+ it("renders the localised label in German too", () => {
+ const html = render("de");
+ expect(html).toContain("Apple Health");
+ });
+});
diff --git a/src/components/measurements/measurement-list.tsx b/src/components/measurements/measurement-list.tsx
index d4067137e..0b14a695a 100644
--- a/src/components/measurements/measurement-list.tsx
+++ b/src/components/measurements/measurement-list.tsx
@@ -106,9 +106,10 @@ function toDateTimeLocalValue(isoString: string): string {
}
/**
- * Render the `MeasurementSource` enum (`MANUAL` / `WITHINGS` / `IMPORT`)
- * using the existing `measurements.source*` translation keys instead of
- * leaking the SCREAMING_SNAKE enum into the table cell.
+ * Render the `MeasurementSource` enum (`MANUAL` / `WITHINGS` / `IMPORT`
+ * / `APPLE_HEALTH`) using the existing `measurements.source*`
+ * translation keys instead of leaking the SCREAMING_SNAKE enum into
+ * the table cell.
*/
function formatMeasurementSource(
source: string,
@@ -117,9 +118,22 @@ function formatMeasurementSource(
if (source === "WITHINGS") return t("measurements.sourceWithings");
if (source === "IMPORT") return t("measurements.sourceImport");
if (source === "MANUAL") return t("measurements.sourceManual");
+ if (source === "APPLE_HEALTH") return t("measurements.sourceAppleHealth");
return source;
}
+/**
+ * Per-source badge colour. `APPLE_HEALTH` gets the Dracula pink that
+ * matches the iOS app's accent; everything else falls back to the
+ * shadcn outline default (no class override).
+ */
+function sourceBadgeClass(source: string): string {
+ if (source === "APPLE_HEALTH") {
+ return "border-dracula-pink/50 bg-dracula-pink/15 text-dracula-pink";
+ }
+ return "";
+}
+
export function MeasurementList({ onEdit, onAddFirst }: MeasurementListProps) {
const { t } = useTranslations();
const fmt = useFormatters();
@@ -431,7 +445,10 @@ export function MeasurementList({ onEdit, onAddFirst }: MeasurementListProps) {
{m.source !== "MANUAL" && (
-
+
{formatMeasurementSource(m.source, t)}
)}