diff --git a/messages/de.json b/messages/de.json index 32eed1133..10b553178 100644 --- a/messages/de.json +++ b/messages/de.json @@ -10155,6 +10155,7 @@ "BLOOD_PRESSURE_SYS": "Blutdruck", "PULSE": "Puls", "BLOOD_GLUCOSE": "Blutzucker", + "BLOOD_KETONES": "Blutketone", "OXYGEN_SATURATION": "Sauerstoffsättigung", "BODY_FAT": "Körperfett", "BODY_TEMPERATURE": "Körpertemperatur", diff --git a/messages/en.json b/messages/en.json index 8dee2871b..3ef7d027e 100644 --- a/messages/en.json +++ b/messages/en.json @@ -10155,6 +10155,7 @@ "BLOOD_PRESSURE_SYS": "Blood pressure", "PULSE": "Pulse", "BLOOD_GLUCOSE": "Blood glucose", + "BLOOD_KETONES": "Blood ketones", "OXYGEN_SATURATION": "Oxygen saturation", "BODY_FAT": "Body fat", "BODY_TEMPERATURE": "Body temperature", diff --git a/messages/es.json b/messages/es.json index c320377af..77e17bb18 100644 --- a/messages/es.json +++ b/messages/es.json @@ -10155,6 +10155,7 @@ "BLOOD_PRESSURE_SYS": "Presión arterial", "PULSE": "Pulso", "BLOOD_GLUCOSE": "Glucosa en sangre", + "BLOOD_KETONES": "Cetonas en sangre", "OXYGEN_SATURATION": "Saturación de oxígeno", "BODY_FAT": "Grasa corporal", "BODY_TEMPERATURE": "Temperatura corporal", diff --git a/messages/fr.json b/messages/fr.json index 7a9a82e03..a868039e6 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -10155,6 +10155,7 @@ "BLOOD_PRESSURE_SYS": "Tension artérielle", "PULSE": "Pouls", "BLOOD_GLUCOSE": "Glycémie", + "BLOOD_KETONES": "Cétones sanguines", "OXYGEN_SATURATION": "Saturation en oxygène", "BODY_FAT": "Masse grasse", "BODY_TEMPERATURE": "Température corporelle", diff --git a/messages/it.json b/messages/it.json index b9f7e6073..de6ead102 100644 --- a/messages/it.json +++ b/messages/it.json @@ -10155,6 +10155,7 @@ "BLOOD_PRESSURE_SYS": "Pressione sanguigna", "PULSE": "Battito", "BLOOD_GLUCOSE": "Glicemia", + "BLOOD_KETONES": "Chetoni nel sangue", "OXYGEN_SATURATION": "Saturazione di ossigeno", "BODY_FAT": "Grasso corporeo", "BODY_TEMPERATURE": "Temperatura corporea", diff --git a/messages/ko.json b/messages/ko.json index 4d05bd352..8d9875804 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -10155,6 +10155,7 @@ "BLOOD_PRESSURE_SYS": "혈압", "PULSE": "맥박", "BLOOD_GLUCOSE": "혈당", + "BLOOD_KETONES": "혈중 케톤", "OXYGEN_SATURATION": "산소포화도", "BODY_FAT": "체지방", "BODY_TEMPERATURE": "체온", diff --git a/messages/nl.json b/messages/nl.json index 66e9ad010..4bdcab77e 100644 --- a/messages/nl.json +++ b/messages/nl.json @@ -10155,6 +10155,7 @@ "BLOOD_PRESSURE_SYS": "Bloeddruk", "PULSE": "Hartslag", "BLOOD_GLUCOSE": "Bloedglucose", + "BLOOD_KETONES": "Bloedketonen", "OXYGEN_SATURATION": "Zuurstofsaturatie", "BODY_FAT": "Vetpercentage", "BODY_TEMPERATURE": "Lichaamstemperatuur", diff --git a/messages/pl.json b/messages/pl.json index 90d724037..53d9ed244 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -10155,6 +10155,7 @@ "BLOOD_PRESSURE_SYS": "Ciśnienie krwi", "PULSE": "Tętno", "BLOOD_GLUCOSE": "Glukoza we krwi", + "BLOOD_KETONES": "Ketony we krwi", "OXYGEN_SATURATION": "Saturacja tlenem", "BODY_FAT": "Tkanka tłuszczowa", "BODY_TEMPERATURE": "Temperatura ciała", diff --git a/messages/pt-BR.json b/messages/pt-BR.json index 36ea5e553..32656f07b 100644 --- a/messages/pt-BR.json +++ b/messages/pt-BR.json @@ -10155,6 +10155,7 @@ "BLOOD_PRESSURE_SYS": "Pressão arterial", "PULSE": "Pulso", "BLOOD_GLUCOSE": "Glicemia", + "BLOOD_KETONES": "Cetonas no sangue", "OXYGEN_SATURATION": "Saturação de oxigênio", "BODY_FAT": "Gordura corporal", "BODY_TEMPERATURE": "Temperatura corporal", diff --git a/messages/ru.json b/messages/ru.json index 431bdbac2..fcf9c31de 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -10155,6 +10155,7 @@ "BLOOD_PRESSURE_SYS": "АД", "PULSE": "Пульс", "BLOOD_GLUCOSE": "Глюкоза крови", + "BLOOD_KETONES": "Кетоны крови", "OXYGEN_SATURATION": "Насыщение кислородом", "BODY_FAT": "Процент жировой массы", "BODY_TEMPERATURE": "Температура тела", diff --git a/src/components/measurement-reminders/__tests__/type-labels-i18n.test.ts b/src/components/measurement-reminders/__tests__/type-labels-i18n.test.ts index 7643df2d2..4d7a2380f 100644 --- a/src/components/measurement-reminders/__tests__/type-labels-i18n.test.ts +++ b/src/components/measurement-reminders/__tests__/type-labels-i18n.test.ts @@ -28,6 +28,7 @@ const LOCALES = readdirSync(MESSAGES_DIR) const REQUIRED_KEYS = [ // Newly added Vorsorge reminder type labels. "measurementReminders.types.WAIST_CIRCUMFERENCE", + "measurementReminders.types.BLOOD_KETONES", "measurementReminders.types.WHO5_SCORE", "measurementReminders.types.SCI_SCORE", // Pre-existing screening labels (regression floor). diff --git a/src/components/measurement-reminders/checkups/reminder-form.ts b/src/components/measurement-reminders/checkups/reminder-form.ts index 946a087e0..8dd80bb9e 100644 --- a/src/components/measurement-reminders/checkups/reminder-form.ts +++ b/src/components/measurement-reminders/checkups/reminder-form.ts @@ -16,6 +16,7 @@ export const TYPE_GROUPS = [ "BLOOD_PRESSURE_SYS", "PULSE", "BLOOD_GLUCOSE", + "BLOOD_KETONES", "OXYGEN_SATURATION", "BODY_TEMPERATURE", ], diff --git a/src/lib/insights/__tests__/metric-catalog.test.ts b/src/lib/insights/__tests__/metric-catalog.test.ts new file mode 100644 index 000000000..d62772cb3 --- /dev/null +++ b/src/lib/insights/__tests__/metric-catalog.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; + +import { METRIC_CATALOG_ENTRIES } from "../metric-catalog"; + +/** + * The metric catalog offers "Connect a source" for a metric without readings + * and the metric's own entry action for one only a person can log. Ketones + * shipped without joining the manual list, so their row sent people to the + * integrations page, where nothing delivers ketones. + */ +describe("metric catalog", () => { + it("offers ketones as a manual entry, not a source to connect", () => { + const ketones = METRIC_CATALOG_ENTRIES.find((e) => e.id === "ketones"); + expect(ketones).toBeDefined(); + expect(ketones?.connectable).toBe(false); + expect(ketones?.manualHref).toBe("/measurements?add=BLOOD_KETONES"); + expect(ketones?.manualCtaKey).toBe("insights.bloodKetones.emptyState.cta"); + }); + + it("gives every metric that cannot be connected a way to log it", () => { + const manualOnly = METRIC_CATALOG_ENTRIES.filter( + (e) => e.kind === "metric" && !e.connectable, + ); + expect(manualOnly.length).toBeGreaterThan(0); + for (const entry of manualOnly) { + expect(entry.manualHref, entry.id).toBeTruthy(); + expect(entry.manualCtaKey, entry.id).toBeTruthy(); + } + }); +}); diff --git a/src/lib/insights/metric-catalog.ts b/src/lib/insights/metric-catalog.ts index 5df3b6061..12b3611f7 100644 --- a/src/lib/insights/metric-catalog.ts +++ b/src/lib/insights/metric-catalog.ts @@ -109,12 +109,13 @@ const SOURCE_KEY_OVERRIDES: Partial> = { /** * Metrics with no device path at all — manual entry is the only source. * Mood and medications are inherently self-report (no sensor produces a - * mood score or an adherence event). Pain, grip strength, and waist-to- - * height ratio are ALSO manual-only despite sitting beside device-fed - * siblings in their group: none carries an Apple Health mapping + * mood score or an adherence event). Pain, grip strength, waist-to-height + * ratio and blood ketones are ALSO manual-only despite sitting beside + * device-fed siblings in their group: none carries an Apple Health mapping * (`apple-health-mapping.ts`) or a Withings/wearable source — pain is a - * 0–10 self-rating, grip strength is read off a hand dynamometer, and - * waist-to-height is logged directly (not derived like BMI). Cross- + * 0–10 self-rating, grip strength is read off a hand dynamometer, + * waist-to-height is logged directly (not derived like BMI), and ketones + * come off a fingerstick meter that no integration reads. Cross- * checked against every routed sub-page's `emptyStateCtaType` + * `emptyState.description` copy so this list can't silently drift from * what the metric's own empty state actually offers. @@ -125,6 +126,7 @@ const MANUAL_ONLY_SLUGS: ReadonlySet = new Set([ "pain", "grip-strength", "waist-to-height", + "ketones", ]); const MANUAL_CTA: Partial< @@ -147,6 +149,10 @@ const MANUAL_CTA: Partial< href: "/measurements?add=WAIST_TO_HEIGHT", ctaKey: "insights.waistToHeight.emptyState.cta", }, + ketones: { + href: "/measurements?add=BLOOD_KETONES", + ctaKey: "insights.bloodKetones.emptyState.cta", + }, }; /** diff --git a/src/lib/validations/__tests__/measurement-reminders.test.ts b/src/lib/validations/__tests__/measurement-reminders.test.ts index 2ff187c10..5ad02edf0 100644 --- a/src/lib/validations/__tests__/measurement-reminders.test.ts +++ b/src/lib/validations/__tests__/measurement-reminders.test.ts @@ -22,6 +22,7 @@ describe("measurementReminderTypeEnum (V3 — completeness)", () => { "BLOOD_PRESSURE_SYS", "PULSE", "BLOOD_GLUCOSE", + "BLOOD_KETONES", "OXYGEN_SATURATION", "BODY_TEMPERATURE", // Body composition diff --git a/src/lib/validations/measurement-reminders.ts b/src/lib/validations/measurement-reminders.ts index 83ae6eb4f..19bce2c80 100644 --- a/src/lib/validations/measurement-reminders.ts +++ b/src/lib/validations/measurement-reminders.ts @@ -45,6 +45,8 @@ export const measurementReminderTypeEnum = z "BLOOD_PRESSURE_SYS", "PULSE", "BLOOD_GLUCOSE", + // Ketones come off the same fingerstick meter as glucose. + "BLOOD_KETONES", "OXYGEN_SATURATION", "BODY_TEMPERATURE", // Body composition (one scale reading produces the whole family)