fix(ketones): offer ketone logging in the catalog and as a checkup

Ketones joined in v1.47.2 without their two sibling lists. The metric
catalog treated them as connectable and sent a user without readings
to the integrations page, where nothing delivers ketones; they now
count as manual-only and the row offers "Log ketones". And a checkup
could not be tied to ketones, so a "measure ketones" reminder never
resolved on its own; ketones are now a reminder target next to blood
glucose, with a label in all ten locales. A catalog test also holds
every metric that cannot be connected to a way to log it.
This commit is contained in:
Marc-André Bombeck committed 2026-11-11 22:34:51 +01:00
1 parent 3edb63d759
commit 2a68b39a85
16 files changed
+56 -5

No files matched your search

+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -10155,6 +10155,7 @@
"BLOOD_PRESSURE_SYS": "혈압",
"PULSE": "맥박",
"BLOOD_GLUCOSE": "혈당",
"BLOOD_KETONES": "혈중 케톤",
"OXYGEN_SATURATION": "산소포화도",
"BODY_FAT": "체지방",
"BODY_TEMPERATURE": "체온",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -10155,6 +10155,7 @@
"BLOOD_PRESSURE_SYS": "АД",
"PULSE": "Пульс",
"BLOOD_GLUCOSE": "Глюкоза крови",
"BLOOD_KETONES": "Кетоны крови",
"OXYGEN_SATURATION": "Насыщение кислородом",
"BODY_FAT": "Процент жировой массы",
"BODY_TEMPERATURE": "Температура тела",
@@ -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).
@@ -16,6 +16,7 @@ export const TYPE_GROUPS = [
"BLOOD_PRESSURE_SYS",
"PULSE",
"BLOOD_GLUCOSE",
"BLOOD_KETONES",
"OXYGEN_SATURATION",
"BODY_TEMPERATURE",
],
@@ -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();
}
});
});
+11 -5
View File
@@ -109,12 +109,13 @@ const SOURCE_KEY_OVERRIDES: Partial<Record<SubPageSlug, string>> = {
/**
* 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<SubPageSlug> = 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",
},
};
/**
@@ -22,6 +22,7 @@ describe("measurementReminderTypeEnum (V3 — completeness)", () => {
"BLOOD_PRESSURE_SYS",
"PULSE",
"BLOOD_GLUCOSE",
"BLOOD_KETONES",
"OXYGEN_SATURATION",
"BODY_TEMPERATURE",
// Body composition
@@ -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)