feat(ui): Apple Health source badge + restore-route enum sync

The measurements list now paints `APPLE_HEALTH` rows with a Dracula-
pink chip whose accent matches the iOS app's brand. The localised
label ships in both English and German via the new
`measurements.sourceAppleHealth` key; the helper falls back to the
SCREAMING_SNAKE value for unknown sources so a missing key would
surface immediately instead of silently rendering blank.

Admin backup restore guards (`MEASUREMENT_TYPES`,
`MEASUREMENT_SOURCES`) gain the seven new HealthKit-shaped types and
the new source value so a restore from a backup that contains Apple
Health rows doesn't get rejected as malformed.

A new badge regression test mirrors the existing BP-badge guard:
asserts the localised label paints, the Dracula-pink utility classes
land, and the bilingual locale still renders the same brand spelling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-11 00:22:58 +02:00
1 parent 5cc0a00eaf
commit 86a80c5280
5 files changed
+125 -5

No files matched your search

+1
View File
@@ -302,6 +302,7 @@
"sourceManual": "Manuell",
"sourceWithings": "Withings",
"sourceImport": "Import",
"sourceAppleHealth": "Apple Health",
"systolicLabel": "Systolisch (mmHg)",
"diastolicLabel": "Diastolisch (mmHg)",
"pulseLabel": "Puls (bpm)",
+1
View File
@@ -302,6 +302,7 @@
"sourceManual": "Manual",
"sourceWithings": "Withings",
"sourceImport": "Import",
"sourceAppleHealth": "Apple Health",
"systolicLabel": "Systolic (mmHg)",
"diastolicLabel": "Diastolic (mmHg)",
"pulseLabel": "Pulse (bpm)",
@@ -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(
@@ -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(
<I18nProvider initialLocale={locale}>
<MeasurementList />
</I18nProvider>,
);
}
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");
});
});
@@ -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) {
</TableCell>
<TableCell>
{m.source !== "MANUAL" && (
<Badge variant="outline" className="text-xs">
<Badge
variant="outline"
className={`text-xs ${sourceBadgeClass(m.source)}`.trim()}
>
{formatMeasurementSource(m.source, t)}
</Badge>
)}