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)} )}