diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml
index 9485410cb..e349dc30a 100644
--- a/docs/api/openapi.yaml
+++ b/docs/api/openapi.yaml
@@ -7184,10 +7184,19 @@ components:
- PULSE
- BLOOD_GLUCOSE
- OXYGEN_SATURATION
- - BODY_FAT
- BODY_TEMPERATURE
- description: Auto-resolve target metric. BP resolves on BLOOD_PRESSURE_SYS (the SYS row is the 'a BP was measured'
- sentinel). Omit for a free-text Vorsorge that resolves only on a manual satisfy.
+ - BODY_FAT
+ - FAT_MASS
+ - FAT_FREE_MASS
+ - MUSCLE_MASS
+ - LEAN_BODY_MASS
+ - BONE_MASS
+ - TOTAL_BODY_WATER
+ - VISCERAL_FAT
+ - BODY_MASS_INDEX
+ description: Auto-resolve target metric (vitals + body-composition family). BP resolves on BLOOD_PRESSURE_SYS (the SYS
+ row is the 'a BP was measured' sentinel). Omit for a free-text Vorsorge that resolves only on a manual satisfy
+ or a matching lab result.
MeasurementReminderUpdate:
type: object
properties:
diff --git a/messages/de.json b/messages/de.json
index 35c813012..c8f36560c 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -6478,7 +6478,19 @@
"BLOOD_GLUCOSE": "Blutzucker",
"OXYGEN_SATURATION": "Sauerstoffsättigung",
"BODY_FAT": "Körperfett",
- "BODY_TEMPERATURE": "Körpertemperatur"
+ "BODY_TEMPERATURE": "Körpertemperatur",
+ "FAT_MASS": "Fettmasse",
+ "FAT_FREE_MASS": "Fettfreie Masse",
+ "MUSCLE_MASS": "Muskelmasse",
+ "LEAN_BODY_MASS": "Magermasse",
+ "BONE_MASS": "Knochenmasse",
+ "TOTAL_BODY_WATER": "Körperwasser",
+ "VISCERAL_FAT": "Viszeralfett",
+ "BODY_MASS_INDEX": "Body-Mass-Index (BMI)"
+ },
+ "typeGroups": {
+ "vitals": "Vitalwerte",
+ "bodyComposition": "Körperzusammensetzung"
},
"empty": {
"title": "Noch keine Erinnerungen",
diff --git a/messages/en.json b/messages/en.json
index ce511ebb8..7b5bbdced 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -6478,7 +6478,19 @@
"BLOOD_GLUCOSE": "Blood glucose",
"OXYGEN_SATURATION": "Oxygen saturation",
"BODY_FAT": "Body fat",
- "BODY_TEMPERATURE": "Body temperature"
+ "BODY_TEMPERATURE": "Body temperature",
+ "FAT_MASS": "Fat mass",
+ "FAT_FREE_MASS": "Fat-free mass",
+ "MUSCLE_MASS": "Muscle mass",
+ "LEAN_BODY_MASS": "Lean body mass",
+ "BONE_MASS": "Bone mass",
+ "TOTAL_BODY_WATER": "Body water",
+ "VISCERAL_FAT": "Visceral fat",
+ "BODY_MASS_INDEX": "Body mass index (BMI)"
+ },
+ "typeGroups": {
+ "vitals": "Vitals",
+ "bodyComposition": "Body composition"
},
"empty": {
"title": "No reminders yet",
diff --git a/messages/es.json b/messages/es.json
index 083fee1bc..27d84b2a8 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -6478,7 +6478,19 @@
"BLOOD_GLUCOSE": "Glucosa en sangre",
"OXYGEN_SATURATION": "Saturación de oxígeno",
"BODY_FAT": "Grasa corporal",
- "BODY_TEMPERATURE": "Temperatura corporal"
+ "BODY_TEMPERATURE": "Temperatura corporal",
+ "FAT_MASS": "Masa grasa",
+ "FAT_FREE_MASS": "Masa libre de grasa",
+ "MUSCLE_MASS": "Masa muscular",
+ "LEAN_BODY_MASS": "Masa magra",
+ "BONE_MASS": "Masa ósea",
+ "TOTAL_BODY_WATER": "Agua corporal",
+ "VISCERAL_FAT": "Grasa visceral",
+ "BODY_MASS_INDEX": "Índice de masa corporal (IMC)"
+ },
+ "typeGroups": {
+ "vitals": "Constantes vitales",
+ "bodyComposition": "Composición corporal"
},
"empty": {
"title": "Aún no hay recordatorios",
diff --git a/messages/fr.json b/messages/fr.json
index 9b837a2c6..25c877947 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -6478,7 +6478,19 @@
"BLOOD_GLUCOSE": "Glycémie",
"OXYGEN_SATURATION": "Saturation en oxygène",
"BODY_FAT": "Masse grasse",
- "BODY_TEMPERATURE": "Température corporelle"
+ "BODY_TEMPERATURE": "Température corporelle",
+ "FAT_MASS": "Masse grasse",
+ "FAT_FREE_MASS": "Masse maigre totale",
+ "MUSCLE_MASS": "Masse musculaire",
+ "LEAN_BODY_MASS": "Masse maigre",
+ "BONE_MASS": "Masse osseuse",
+ "TOTAL_BODY_WATER": "Eau corporelle",
+ "VISCERAL_FAT": "Graisse viscérale",
+ "BODY_MASS_INDEX": "Indice de masse corporelle (IMC)"
+ },
+ "typeGroups": {
+ "vitals": "Constantes vitales",
+ "bodyComposition": "Composition corporelle"
},
"empty": {
"title": "Aucun rappel pour l'instant",
diff --git a/messages/it.json b/messages/it.json
index 23305e7da..2f27bd97c 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -6478,7 +6478,19 @@
"BLOOD_GLUCOSE": "Glicemia",
"OXYGEN_SATURATION": "Saturazione di ossigeno",
"BODY_FAT": "Grasso corporeo",
- "BODY_TEMPERATURE": "Temperatura corporea"
+ "BODY_TEMPERATURE": "Temperatura corporea",
+ "FAT_MASS": "Massa grassa",
+ "FAT_FREE_MASS": "Massa magra totale",
+ "MUSCLE_MASS": "Massa muscolare",
+ "LEAN_BODY_MASS": "Massa magra",
+ "BONE_MASS": "Massa ossea",
+ "TOTAL_BODY_WATER": "Acqua corporea",
+ "VISCERAL_FAT": "Grasso viscerale",
+ "BODY_MASS_INDEX": "Indice di massa corporea (IMC)"
+ },
+ "typeGroups": {
+ "vitals": "Parametri vitali",
+ "bodyComposition": "Composizione corporea"
},
"empty": {
"title": "Ancora nessun promemoria",
diff --git a/messages/pl.json b/messages/pl.json
index 8e1ff3360..2a5354dbc 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -6478,7 +6478,19 @@
"BLOOD_GLUCOSE": "Glukoza we krwi",
"OXYGEN_SATURATION": "Saturacja tlenem",
"BODY_FAT": "Tkanka tłuszczowa",
- "BODY_TEMPERATURE": "Temperatura ciała"
+ "BODY_TEMPERATURE": "Temperatura ciała",
+ "FAT_MASS": "Masa tłuszczowa",
+ "FAT_FREE_MASS": "Masa beztłuszczowa",
+ "MUSCLE_MASS": "Masa mięśniowa",
+ "LEAN_BODY_MASS": "Masa szczupła",
+ "BONE_MASS": "Masa kostna",
+ "TOTAL_BODY_WATER": "Woda w organizmie",
+ "VISCERAL_FAT": "Tłuszcz trzewny",
+ "BODY_MASS_INDEX": "Wskaźnik masy ciała (BMI)"
+ },
+ "typeGroups": {
+ "vitals": "Parametry życiowe",
+ "bodyComposition": "Skład ciała"
},
"empty": {
"title": "Brak przypomnień",
diff --git a/src/components/measurement-reminders/vorsorge-section.tsx b/src/components/measurement-reminders/vorsorge-section.tsx
index 43bd02fa6..2a9c85b7b 100644
--- a/src/components/measurement-reminders/vorsorge-section.tsx
+++ b/src/components/measurement-reminders/vorsorge-section.tsx
@@ -38,14 +38,37 @@ import {
type MeasurementReminder,
} from "@/hooks/use-measurement-reminders";
-const TYPE_OPTIONS = [
- "WEIGHT",
- "BLOOD_PRESSURE_SYS",
- "PULSE",
- "BLOOD_GLUCOSE",
- "OXYGEN_SATURATION",
- "BODY_FAT",
- "BODY_TEMPERATURE",
+// v1.18.1 (V3) — the active-measurement set, grouped so the dropdown
+// doesn't read as a flat 15-item list. Mirrors the Zod allow-list in
+// `validations/measurement-reminders.ts`; the labels resolve through
+// `measurementReminders.types.*`, the group headers through
+// `measurementReminders.typeGroups.*`.
+const TYPE_GROUPS = [
+ {
+ group: "vitals",
+ types: [
+ "WEIGHT",
+ "BLOOD_PRESSURE_SYS",
+ "PULSE",
+ "BLOOD_GLUCOSE",
+ "OXYGEN_SATURATION",
+ "BODY_TEMPERATURE",
+ ],
+ },
+ {
+ group: "bodyComposition",
+ types: [
+ "BODY_FAT",
+ "FAT_MASS",
+ "FAT_FREE_MASS",
+ "MUSCLE_MASS",
+ "LEAN_BODY_MASS",
+ "BONE_MASS",
+ "TOTAL_BODY_WATER",
+ "VISCERAL_FAT",
+ "BODY_MASS_INDEX",
+ ],
+ },
] as const;
const INTERVAL_PRESETS = [7, 14, 30, 90, 180, 365] as const;
@@ -220,10 +243,17 @@ export function VorsorgeSection({
value={measurementType}
onChange={(e) => setMeasurementType(e.target.value)}
>
- {TYPE_OPTIONS.map((type) => (
-
+ {TYPE_GROUPS.map(({ group, types }) => (
+
))}
diff --git a/src/lib/validations/__tests__/measurement-reminders.test.ts b/src/lib/validations/__tests__/measurement-reminders.test.ts
new file mode 100644
index 000000000..219e395ee
--- /dev/null
+++ b/src/lib/validations/__tests__/measurement-reminders.test.ts
@@ -0,0 +1,77 @@
+/**
+ * v1.18.1 (V3) — the Vorsorge measurement-type allow-list. Widened from
+ * the original 7 to the ~15 active-measurement set (vitals + the
+ * body-composition family). App-layer only — the DB column is the full
+ * MeasurementType enum, so this is the gate, not a migration.
+ */
+import { describe, expect, it } from "vitest";
+
+import {
+ measurementReminderTypeEnum,
+ createMeasurementReminderSchema,
+} from "../measurement-reminders";
+
+describe("measurementReminderTypeEnum (V3 — completeness)", () => {
+ const allowed = measurementReminderTypeEnum.options;
+
+ it("exposes the full active set: vitals + body composition", () => {
+ expect(new Set(allowed)).toEqual(
+ new Set([
+ // Vitals
+ "WEIGHT",
+ "BLOOD_PRESSURE_SYS",
+ "PULSE",
+ "BLOOD_GLUCOSE",
+ "OXYGEN_SATURATION",
+ "BODY_TEMPERATURE",
+ // Body composition
+ "BODY_FAT",
+ "FAT_MASS",
+ "FAT_FREE_MASS",
+ "MUSCLE_MASS",
+ "LEAN_BODY_MASS",
+ "BONE_MASS",
+ "TOTAL_BODY_WATER",
+ "VISCERAL_FAT",
+ "BODY_MASS_INDEX",
+ ]),
+ );
+ });
+
+ it("accepts a newly-added body-composition type on create", () => {
+ const parsed = createMeasurementReminderSchema.safeParse({
+ label: "Körperzusammensetzung messen",
+ measurementType: "MUSCLE_MASS",
+ intervalDays: 7,
+ });
+ expect(parsed.success).toBe(true);
+ });
+
+ it("matches BP on the SYS sentinel only (DIA would double-count)", () => {
+ expect(allowed).toContain("BLOOD_PRESSURE_SYS");
+ expect(allowed).not.toContain("BLOOD_PRESSURE_DIA");
+ });
+
+ it("deliberately excludes passive / cumulative / event types", () => {
+ for (const excluded of [
+ "RESTING_HEART_RATE",
+ "HEART_RATE_VARIABILITY",
+ "VO2_MAX",
+ "ACTIVITY_STEPS",
+ "RECOVERY_SCORE",
+ "SLEEP_DURATION",
+ "IRREGULAR_RHYTHM_NOTIFICATION",
+ ]) {
+ expect(allowed).not.toContain(excluded);
+ }
+ });
+
+ it("still accepts a free-text reminder (null type) resolving on satisfy / lab", () => {
+ const parsed = createMeasurementReminderSchema.safeParse({
+ label: "Großes Blutbild",
+ measurementType: null,
+ rrule: "FREQ=YEARLY",
+ });
+ expect(parsed.success).toBe(true);
+ });
+});
diff --git a/src/lib/validations/measurement-reminders.ts b/src/lib/validations/measurement-reminders.ts
index fe295c189..e8fa7c5a9 100644
--- a/src/lib/validations/measurement-reminders.ts
+++ b/src/lib/validations/measurement-reminders.ts
@@ -17,28 +17,50 @@ import { z } from "zod/v4";
/**
* The auto-resolve target types. Kept as an explicit allow-list rather
* than the full `MeasurementType` enum: a Vorsorge reminder only makes
- * sense for the metrics a user actively measures on a cadence (BP,
- * weight, glucose, pulse, SpO2, body composition, body temperature).
- * Free-text reminders pass `null` and resolve only on a manual satisfy.
+ * sense for the metrics a user actively goes and measures on a cadence —
+ * vitals + the body-composition family a scale produces.
+ *
+ * v1.18.1 (V3) widens the original 7 to the full ~15 active-measurement
+ * set. The DB column is the full `MeasurementType` enum, so this is a
+ * pure application-layer change — no migration.
+ *
+ * DELIBERATELY excluded: passive wearable scores + cumulative activity +
+ * device events (RECOVERY_SCORE, ACTIVITY_STEPS, IRREGULAR_RHYTHM_*, …),
+ * and the passive nightly samples RESTING_HEART_RATE / HEART_RATE_VARIABILITY
+ * / VO2_MAX — nudging a user to "go measure" a passive sample makes no
+ * sense (they still auto-satisfy any free-text reminder via a LabResult if
+ * ever wanted, but they are not "remind me to measure" targets).
*
* BP is matched on `BLOOD_PRESSURE_SYS` as the canonical sentinel — a BP
* reading is two rows (SYS + DIA) and matching either would double-count;
- * SYS is the agreed "a BP was measured" anchor.
+ * SYS is the agreed "a BP was measured" anchor. A single step-on-the-scale
+ * event writes WEIGHT + several body-composition rows at once, so any one
+ * of them satisfies its linked reminder.
*/
export const measurementReminderTypeEnum = z
.enum([
+ // Vitals
"WEIGHT",
"BLOOD_PRESSURE_SYS",
"PULSE",
"BLOOD_GLUCOSE",
"OXYGEN_SATURATION",
- "BODY_FAT",
"BODY_TEMPERATURE",
+ // Body composition (one scale reading produces the whole family)
+ "BODY_FAT",
+ "FAT_MASS",
+ "FAT_FREE_MASS",
+ "MUSCLE_MASS",
+ "LEAN_BODY_MASS",
+ "BONE_MASS",
+ "TOTAL_BODY_WATER",
+ "VISCERAL_FAT",
+ "BODY_MASS_INDEX",
])
.meta({
id: "MeasurementReminderType",
description:
- "Auto-resolve target metric. BP resolves on BLOOD_PRESSURE_SYS (the SYS row is the 'a BP was measured' sentinel). Omit for a free-text Vorsorge that resolves only on a manual satisfy.",
+ "Auto-resolve target metric (vitals + body-composition family). BP resolves on BLOOD_PRESSURE_SYS (the SYS row is the 'a BP was measured' sentinel). Omit for a free-text Vorsorge that resolves only on a manual satisfy or a matching lab result.",
});
export type MeasurementReminderType = z.infer<