diff --git a/next.config.ts b/next.config.ts index c37098a14..7440d153a 100644 --- a/next.config.ts +++ b/next.config.ts @@ -53,6 +53,65 @@ const nextConfig: NextConfig = { // from memory instead of paying a full reload. See // `src/lib/http/cache-headers.ts` for the typed constant reused by // route handlers that opt into the same posture. + // v1.8.0 — the routed Insights sub-pages migrated from German to + // English slugs (`/insights/blutdruck` → `/insights/blood-pressure`, + // …). Every legacy German URL 301-redirects to its English target so + // bookmarks, the PWA's cached navigation, and any external link keep + // resolving. The redirect set is exhaustive and matches the rename + // table in `docs/adr/0001-insights-naming-convention.md`; the slug + // registry itself lives in `src/lib/insights/sub-page-metric.ts`. + // `bmi`, `hrv`, and `workouts` were already English and need no entry. + async redirects() { + const insightsSlugRenames: Array<[string, string]> = [ + ["blutdruck", "blood-pressure"], + ["puls", "pulse"], + ["sauerstoff", "oxygen"], + ["koerpertemperatur", "body-temperature"], + ["atemfrequenz", "respiratory-rate"], + ["gewicht", "weight"], + ["koerperwasser", "body-water"], + ["knochenmasse", "bone-mass"], + ["fettfreie-masse", "fat-free-mass"], + ["fettmasse", "fat-mass"], + ["muskelmasse", "muscle-mass"], + ["viszeralfett", "visceral-fat"], + ["magermasse", "lean-body-mass"], + ["aktive-energie", "active-energy"], + ["stockwerke", "flights-climbed"], + ["gehstrecke", "walking-distance"], + ["gangstabilitaet", "walking-steadiness"], + ["gehpuls", "walking-heart-rate"], + ["gangasymmetrie", "walking-asymmetry"], + ["doppelstandphase", "double-support-time"], + ["schrittlaenge", "step-length"], + ["gehgeschwindigkeit", "walking-speed"], + ["schlaf", "sleep"], + ["ruhepuls", "resting-pulse"], + ["pulswellengeschwindigkeit", "pulse-wave-velocity"], + ["gefaessalter", "vascular-age"], + ["laermbelastung", "environmental-audio"], + ["kopfhoererpegel", "headphone-audio"], + ["laermereignisse", "audio-events"], + ["tageslicht", "daylight"], + ["blutzucker", "blood-glucose"], + ["hauttemperatur", "skin-temperature"], + ["stimmung", "mood"], + ["medikamente", "medications"], + ]; + return insightsSlugRenames.flatMap(([from, to]) => [ + { + source: `/insights/${from}`, + destination: `/insights/${to}`, + permanent: true, + }, + // Preserve any nested path (e.g. a future `/insights/sleep/2026-05-31`). + { + source: `/insights/${from}/:path*`, + destination: `/insights/${to}/:path*`, + permanent: true, + }, + ]); + }, async headers() { return [ { diff --git a/src/app/api/insights/glp1-timeline/route.ts b/src/app/api/insights/glp1-timeline/route.ts index 26f291608..45d18cb43 100644 --- a/src/app/api/insights/glp1-timeline/route.ts +++ b/src/app/api/insights/glp1-timeline/route.ts @@ -4,7 +4,7 @@ * Returns a chronological merge of every GLP-1-relevant event the user * has logged: dose changes, injections (with site if recorded), * inventory events, and side-effect-tagged mood entries from the - * trailing 90 days. Used by /insights/medikamente's TherapyTimeline + * trailing 90 days. Used by /insights/medications' TherapyTimeline * component. Web-only users without a GLP-1 medication get * `hasGlp1: false` so the component hides cleanly. */ diff --git a/src/app/insights/aktive-energie/page.tsx b/src/app/insights/active-energy/page.tsx similarity index 96% rename from src/app/insights/aktive-energie/page.tsx rename to src/app/insights/active-energy/page.tsx index 4ce571b3c..1fcf9dc01 100644 --- a/src/app/insights/aktive-energie/page.tsx +++ b/src/app/insights/active-energy/page.tsx @@ -5,7 +5,7 @@ import { Flame } from "lucide-react"; import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page"; /** - * v1.4.32 — `/insights/aktive-energie`. + * v1.4.32 — `/insights/active-energy`. * * Active-energy-burned sub-page. The MeasurementType is cumulative * (kcal per day, per R-A Option A); the v1.4.30 server-side daily- diff --git a/src/app/insights/laermereignisse/page.tsx b/src/app/insights/audio-events/page.tsx similarity index 95% rename from src/app/insights/laermereignisse/page.tsx rename to src/app/insights/audio-events/page.tsx index 458822f9c..3fcf04442 100644 --- a/src/app/insights/laermereignisse/page.tsx +++ b/src/app/insights/audio-events/page.tsx @@ -5,7 +5,7 @@ import { Volume2 } from "lucide-react"; import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page"; /** - * v1.7.0 — `/insights/laermereignisse`. + * v1.7.0 — `/insights/audio-events`. * * AUDIO_EXPOSURE_EVENT sub-page. Reuses the generic HealthKitMetricPage scaffold; * the chart daily-aggregates through the rollup read path. Empty-state diff --git a/src/app/insights/blutzucker/page.tsx b/src/app/insights/blood-glucose/page.tsx similarity index 95% rename from src/app/insights/blutzucker/page.tsx rename to src/app/insights/blood-glucose/page.tsx index 504e9c5ff..2ede714d6 100644 --- a/src/app/insights/blutzucker/page.tsx +++ b/src/app/insights/blood-glucose/page.tsx @@ -5,7 +5,7 @@ import { Droplet } from "lucide-react"; import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page"; /** - * v1.7.0 — `/insights/blutzucker`. + * v1.7.0 — `/insights/blood-glucose`. * * BLOOD_GLUCOSE sub-page. Reuses the generic HealthKitMetricPage scaffold; * the chart daily-aggregates through the rollup read path. Empty-state diff --git a/src/app/insights/blutdruck/page.tsx b/src/app/insights/blood-pressure/page.tsx similarity index 97% rename from src/app/insights/blutdruck/page.tsx rename to src/app/insights/blood-pressure/page.tsx index e8762e3e7..dc7b4eee1 100644 --- a/src/app/insights/blutdruck/page.tsx +++ b/src/app/insights/blood-pressure/page.tsx @@ -18,7 +18,7 @@ import { SubPageShell } from "@/components/insights/sub-page-shell"; import { getBpTargets } from "@/lib/analytics/bp-targets"; /** - * v1.4.25 W4 — `/insights/blutdruck`. + * v1.4.25 W4 — `/insights/blood-pressure`. * * Routed Blood-Pressure sub-page. The mother page hosts the overview * (hero + briefing + advisor); this page concentrates on BP-only depth: @@ -114,7 +114,7 @@ export default function InsightsBlutdruckPage() { userTimezone={user?.timezone} /> - + { const medicationSummary = medicationSummaryById.get(med.id); // UI-H3 — route every medication-list row (medications page - // + insights/medikamente) through `` + // + insights/medications) through `` // so the surface stays one shape: `{name} {dose}` on line 1 // + outline category badge on line 2. The streak chip rides // the `stateBadges` slot so a strong streak still surfaces. @@ -243,7 +243,7 @@ export default function InsightsMedikamentePage() { })} - + - + - + - + {/* v1.4.28 (BK-UI-StatusSchlaf) — the six sibling sub-pages all diff --git a/src/app/insights/schrittlaenge/page.tsx b/src/app/insights/step-length/page.tsx similarity index 95% rename from src/app/insights/schrittlaenge/page.tsx rename to src/app/insights/step-length/page.tsx index aecb395b7..276610342 100644 --- a/src/app/insights/schrittlaenge/page.tsx +++ b/src/app/insights/step-length/page.tsx @@ -5,7 +5,7 @@ import { Footprints } from "lucide-react"; import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page"; /** - * v1.7.0 — `/insights/schrittlaenge`. + * v1.7.0 — `/insights/step-length`. * * WALKING_STEP_LENGTH sub-page. Reuses the generic HealthKitMetricPage scaffold; * the chart daily-aggregates through the rollup read path. Empty-state diff --git a/src/app/insights/gefaessalter/page.tsx b/src/app/insights/vascular-age/page.tsx similarity index 95% rename from src/app/insights/gefaessalter/page.tsx rename to src/app/insights/vascular-age/page.tsx index 939f14ca1..397271e11 100644 --- a/src/app/insights/gefaessalter/page.tsx +++ b/src/app/insights/vascular-age/page.tsx @@ -5,7 +5,7 @@ import { HeartPulse } from "lucide-react"; import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page"; /** - * v1.7.0 — `/insights/gefaessalter`. + * v1.7.0 — `/insights/vascular-age`. * * VASCULAR_AGE sub-page. Reuses the generic HealthKitMetricPage scaffold; * the chart daily-aggregates through the rollup read path. Empty-state diff --git a/src/app/insights/viszeralfett/page.tsx b/src/app/insights/visceral-fat/page.tsx similarity index 95% rename from src/app/insights/viszeralfett/page.tsx rename to src/app/insights/visceral-fat/page.tsx index bc1bf20d4..3fb062cff 100644 --- a/src/app/insights/viszeralfett/page.tsx +++ b/src/app/insights/visceral-fat/page.tsx @@ -5,7 +5,7 @@ import { Droplets } from "lucide-react"; import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page"; /** - * v1.7.0 — `/insights/viszeralfett`. + * v1.7.0 — `/insights/visceral-fat`. * * VISCERAL_FAT sub-page. Reuses the generic HealthKitMetricPage scaffold; * the chart daily-aggregates through the rollup read path. Empty-state diff --git a/src/app/insights/gangasymmetrie/page.tsx b/src/app/insights/walking-asymmetry/page.tsx similarity index 95% rename from src/app/insights/gangasymmetrie/page.tsx rename to src/app/insights/walking-asymmetry/page.tsx index 5b6ecb548..7182a8e8d 100644 --- a/src/app/insights/gangasymmetrie/page.tsx +++ b/src/app/insights/walking-asymmetry/page.tsx @@ -5,7 +5,7 @@ import { Footprints } from "lucide-react"; import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page"; /** - * v1.7.0 — `/insights/gangasymmetrie`. + * v1.7.0 — `/insights/walking-asymmetry`. * * WALKING_ASYMMETRY sub-page. Reuses the generic HealthKitMetricPage scaffold; * the chart daily-aggregates through the rollup read path. Empty-state diff --git a/src/app/insights/gehstrecke/page.tsx b/src/app/insights/walking-distance/page.tsx similarity index 96% rename from src/app/insights/gehstrecke/page.tsx rename to src/app/insights/walking-distance/page.tsx index f43bf76c8..6280cc0a8 100644 --- a/src/app/insights/gehstrecke/page.tsx +++ b/src/app/insights/walking-distance/page.tsx @@ -5,7 +5,7 @@ import { Footprints } from "lucide-react"; import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page"; /** - * v1.7.0 — `/insights/gehstrecke`. + * v1.7.0 — `/insights/walking-distance`. * * WALKING_RUNNING_DISTANCE sub-page. Reuses the generic HealthKitMetricPage scaffold; * the chart daily-aggregates through the rollup read path. Empty-state diff --git a/src/app/insights/gehpuls/page.tsx b/src/app/insights/walking-heart-rate/page.tsx similarity index 95% rename from src/app/insights/gehpuls/page.tsx rename to src/app/insights/walking-heart-rate/page.tsx index 1f150cb8c..c7c442671 100644 --- a/src/app/insights/gehpuls/page.tsx +++ b/src/app/insights/walking-heart-rate/page.tsx @@ -5,7 +5,7 @@ import { HeartPulse } from "lucide-react"; import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page"; /** - * v1.7.0 — `/insights/gehpuls`. + * v1.7.0 — `/insights/walking-heart-rate`. * * WALKING_HEART_RATE_AVERAGE sub-page. Reuses the generic HealthKitMetricPage scaffold; * the chart daily-aggregates through the rollup read path. Empty-state diff --git a/src/app/insights/gehgeschwindigkeit/page.tsx b/src/app/insights/walking-speed/page.tsx similarity index 95% rename from src/app/insights/gehgeschwindigkeit/page.tsx rename to src/app/insights/walking-speed/page.tsx index 0602540a0..c4fc56b9e 100644 --- a/src/app/insights/gehgeschwindigkeit/page.tsx +++ b/src/app/insights/walking-speed/page.tsx @@ -5,7 +5,7 @@ import { Gauge } from "lucide-react"; import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page"; /** - * v1.7.0 — `/insights/gehgeschwindigkeit`. + * v1.7.0 — `/insights/walking-speed`. * * WALKING_SPEED sub-page. Reuses the generic HealthKitMetricPage scaffold; * the chart daily-aggregates through the rollup read path. Empty-state diff --git a/src/app/insights/gangstabilitaet/page.tsx b/src/app/insights/walking-steadiness/page.tsx similarity index 95% rename from src/app/insights/gangstabilitaet/page.tsx rename to src/app/insights/walking-steadiness/page.tsx index c3e88c47d..fed1ed7ea 100644 --- a/src/app/insights/gangstabilitaet/page.tsx +++ b/src/app/insights/walking-steadiness/page.tsx @@ -5,7 +5,7 @@ import { Gauge } from "lucide-react"; import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page"; /** - * v1.7.0 — `/insights/gangstabilitaet`. + * v1.7.0 — `/insights/walking-steadiness`. * * WALKING_STEADINESS sub-page. Reuses the generic HealthKitMetricPage scaffold; * the chart daily-aggregates through the rollup read path. Empty-state diff --git a/src/app/insights/gewicht/page.tsx b/src/app/insights/weight/page.tsx similarity index 97% rename from src/app/insights/gewicht/page.tsx rename to src/app/insights/weight/page.tsx index 44405ee3d..5a4b01055 100644 --- a/src/app/insights/gewicht/page.tsx +++ b/src/app/insights/weight/page.tsx @@ -18,7 +18,7 @@ import { SubPageShell } from "@/components/insights/sub-page-shell"; import { buildWeightBandsFromHeight } from "@/lib/analytics/value-bands"; /** - * v1.4.25 W4 — `/insights/gewicht`. + * v1.4.25 W4 — `/insights/weight`. * * Routed Weight sub-page. Renders the weight chart with the user's * height-derived green/orange/red bands plus the per-section AI @@ -87,7 +87,7 @@ export default function InsightsGewichtPage() { userTimezone={user?.timezone} /> - + ", () => { it("renders the numeric range + in-target share for a simple metric", () => { - const html = renderWith("gewicht", WEIGHT_DATA); + const html = renderWith("weight", WEIGHT_DATA); // Trailing `.0` trimmed, single-decimal kept. expect(html).toContain("Target: 60.1–80.9 kg"); // round(18/24*100) = 75 @@ -88,19 +88,19 @@ describe("", () => { }); it("stitches the systolic + diastolic bands for blood pressure", () => { - const html = renderWith("blutdruck", BP_DATA); + const html = renderWith("blood-pressure", BP_DATA); expect(html).toContain("Target: 120–129 / 70–79 mmHg"); expect(html).toContain("50% of logged days within target"); }); it("renders the adjust-target link to /targets", () => { - const html = renderWith("gewicht", WEIGHT_DATA); + const html = renderWith("weight", WEIGHT_DATA); expect(html).toContain('href="/targets"'); expect(html).toContain("Adjust target range"); }); it("suppresses the share when the route flags insufficient data", () => { - const html = renderWith("gewicht", { + const html = renderWith("weight", { ...WEIGHT_DATA, targets: [{ ...WEIGHT_DATA.targets[0], insufficientData: true }], }); @@ -114,12 +114,12 @@ describe("", () => { }); it("renders nothing before the payload resolves", () => { - const html = renderWith("gewicht", undefined); + const html = renderWith("weight", undefined); expect(html).toBe(""); }); it("renders nothing when the matching target has no range", () => { - const html = renderWith("gewicht", { + const html = renderWith("weight", { ...WEIGHT_DATA, targets: [{ ...WEIGHT_DATA.targets[0], range: null }], }); diff --git a/src/components/insights/daily-briefing.tsx b/src/components/insights/daily-briefing.tsx index 846e99a9d..7c83dfc13 100644 --- a/src/components/insights/daily-briefing.tsx +++ b/src/components/insights/daily-briefing.tsx @@ -39,23 +39,23 @@ import type { * through to `null` and render as a plain row (no link wrap). */ const METRIC_HREF: Record = { - bp: "/insights/blutdruck", - weight: "/insights/gewicht", - pulse: "/insights/puls", - mood: "/insights/stimmung", - compliance: "/insights/medikamente", + bp: "/insights/blood-pressure", + weight: "/insights/weight", + pulse: "/insights/pulse", + mood: "/insights/mood", + compliance: "/insights/medications", // Apple Health / GLP-1 additive metrics — no dedicated sub-page yet. // They render as static rows; v1.4.28 can wire each as they ship. hrv: null, - sleep: "/insights/schlaf", + sleep: "/insights/sleep", resting_hr: null, steps: null, active_energy: null, flights: null, distance: null, - vo2_max: "/insights/puls", + vo2_max: "/insights/pulse", body_temp: null, - glp1_plateau: "/insights/medikamente", + glp1_plateau: "/insights/medications", }; /** diff --git a/src/components/insights/insights-tab-strip.tsx b/src/components/insights/insights-tab-strip.tsx index e1c6af69f..77613690f 100644 --- a/src/components/insights/insights-tab-strip.tsx +++ b/src/components/insights/insights-tab-strip.tsx @@ -31,7 +31,7 @@ import { * v1.4.25 W4 — routed tab strip for `/insights`. * * Behaviour evolved from the v1.4.25 W3 scroll-anchor version: each pill - * is now a `` to a routed sub-page (`/insights/blutdruck` …), + * is now a `` to a routed sub-page (`/insights/blood-pressure` …), * `usePathname()` decides the active pill, and the strip is mounted in * `src/app/insights/layout.tsx` so it persists across navigation * without re-rendering. The CoachDrawer is NOT mounted in this layout — @@ -116,47 +116,47 @@ const SUB_PAGE_TABS: Record< { labelKey: string; metric: InsightMetric } > = { // ── vitals ── - blutdruck: { + "blood-pressure": { labelKey: "insights.navBloodPressure", metric: "BLOOD_PRESSURE_SYS", }, - puls: { labelKey: "insights.navPulse", metric: "PULSE" }, - sauerstoff: { + pulse: { labelKey: "insights.navPulse", metric: "PULSE" }, + oxygen: { labelKey: "insights.navOxygenSaturation", metric: "OXYGEN_SATURATION", }, - koerpertemperatur: { + "body-temperature": { labelKey: "insights.navBodyTemperature", metric: "BODY_TEMPERATURE", }, - atemfrequenz: { + "respiratory-rate": { labelKey: "insights.navRespiratoryRate", metric: "RESPIRATORY_RATE", }, // ── body composition ── - gewicht: { labelKey: "insights.navWeight", metric: "WEIGHT" }, + weight: { labelKey: "insights.navWeight", metric: "WEIGHT" }, bmi: { labelKey: "insights.navBmi", metric: "BMI" }, - koerperwasser: { + "body-water": { labelKey: "insights.navTotalBodyWater", metric: "TOTAL_BODY_WATER", }, - knochenmasse: { labelKey: "insights.navBoneMass", metric: "BONE_MASS" }, - "fettfreie-masse": { + "bone-mass": { labelKey: "insights.navBoneMass", metric: "BONE_MASS" }, + "fat-free-mass": { labelKey: "insights.navFatFreeMass", metric: "FAT_FREE_MASS", }, - fettmasse: { labelKey: "insights.navFatMass", metric: "FAT_MASS" }, - muskelmasse: { labelKey: "insights.navMuscleMass", metric: "MUSCLE_MASS" }, - viszeralfett: { + "fat-mass": { labelKey: "insights.navFatMass", metric: "FAT_MASS" }, + "muscle-mass": { labelKey: "insights.navMuscleMass", metric: "MUSCLE_MASS" }, + "visceral-fat": { labelKey: "insights.navVisceralFat", metric: "VISCERAL_FAT", }, - magermasse: { + "lean-body-mass": { labelKey: "insights.navLeanBodyMass", metric: "LEAN_BODY_MASS", }, // ── activity ── - "aktive-energie": { + "active-energy": { labelKey: "insights.navActiveEnergy", metric: "ACTIVE_ENERGY_BURNED", }, @@ -164,85 +164,85 @@ const SUB_PAGE_TABS: Record< // availability helper reads `inputs.hasWorkouts` rather than a // `summaries[…].count`. workouts: { labelKey: "insights.navWorkouts", metric: "WORKOUTS" }, - stockwerke: { + "flights-climbed": { labelKey: "insights.navFlightsClimbed", metric: "FLIGHTS_CLIMBED", }, - gehstrecke: { + "walking-distance": { labelKey: "insights.navWalkingRunningDistance", metric: "WALKING_RUNNING_DISTANCE", }, - gangstabilitaet: { + "walking-steadiness": { labelKey: "insights.navWalkingSteadiness", metric: "WALKING_STEADINESS", }, - gehpuls: { + "walking-heart-rate": { labelKey: "insights.navWalkingHeartRateAverage", metric: "WALKING_HEART_RATE_AVERAGE", }, - gangasymmetrie: { + "walking-asymmetry": { labelKey: "insights.navWalkingAsymmetry", metric: "WALKING_ASYMMETRY", }, - doppelstandphase: { + "double-support-time": { labelKey: "insights.navWalkingDoubleSupport", metric: "WALKING_DOUBLE_SUPPORT", }, - schrittlaenge: { + "step-length": { labelKey: "insights.navWalkingStepLength", metric: "WALKING_STEP_LENGTH", }, - gehgeschwindigkeit: { + "walking-speed": { labelKey: "insights.navWalkingSpeed", metric: "WALKING_SPEED", }, // ── sleep ── - schlaf: { labelKey: "insights.navSleep", metric: "SLEEP_DURATION" }, + sleep: { labelKey: "insights.navSleep", metric: "SLEEP_DURATION" }, // ── cardiovascular ── - ruhepuls: { + "resting-pulse": { labelKey: "insights.navRestingHr", metric: "RESTING_HEART_RATE", }, hrv: { labelKey: "insights.navHrv", metric: "HEART_RATE_VARIABILITY" }, - pulswellengeschwindigkeit: { + "pulse-wave-velocity": { labelKey: "insights.navPulseWaveVelocity", metric: "PULSE_WAVE_VELOCITY", }, - gefaessalter: { + "vascular-age": { labelKey: "insights.navVascularAge", metric: "VASCULAR_AGE", }, // ── hearing ── - laermbelastung: { + "environmental-audio": { labelKey: "insights.navAudioExposureEnv", metric: "AUDIO_EXPOSURE_ENV", }, - kopfhoererpegel: { + "headphone-audio": { labelKey: "insights.navAudioExposureHeadphone", metric: "AUDIO_EXPOSURE_HEADPHONE", }, - laermereignisse: { + "audio-events": { labelKey: "insights.navAudioExposureEvent", metric: "AUDIO_EXPOSURE_EVENT", }, // ── environment ── - tageslicht: { + daylight: { labelKey: "insights.navTimeInDaylight", metric: "TIME_IN_DAYLIGHT", }, // ── metabolic ── - blutzucker: { + "blood-glucose": { labelKey: "insights.navBloodGlucose", metric: "BLOOD_GLUCOSE", }, - hauttemperatur: { + "skin-temperature": { labelKey: "insights.navSkinTemperature", metric: "SKIN_TEMPERATURE", }, // ── mood ── - stimmung: { labelKey: "insights.navMood", metric: "MOOD" }, + mood: { labelKey: "insights.navMood", metric: "MOOD" }, // ── events ── - medikamente: { labelKey: "insights.navMedication", metric: "MEDICATION" }, + medications: { labelKey: "insights.navMedication", metric: "MEDICATION" }, }; /** @@ -409,7 +409,7 @@ function InsightsTabStripImpl({ if (tab.kind === "link") { // The overview pill matches the mother page exactly; the // sub-page pills match a prefix so future nested routes - // (e.g. `/insights/schlaf/2026-05-11`) still highlight the + // (e.g. `/insights/sleep/2026-05-11`) still highlight the // parent tab. const isActive = tab.href === INSIGHTS_OVERVIEW_PATH diff --git a/src/components/insights/metric-target-summary.tsx b/src/components/insights/metric-target-summary.tsx index a5328b290..d1b5fe0bb 100644 --- a/src/components/insights/metric-target-summary.tsx +++ b/src/components/insights/metric-target-summary.tsx @@ -67,17 +67,17 @@ interface TargetsResponse { * component renders nothing for them, so the sub-page collapses cleanly. */ const SLUG_TO_TARGET_TYPE: Record = { - blutdruck: "BLOOD_PRESSURE", - gewicht: "WEIGHT", + "blood-pressure": "BLOOD_PRESSURE", + weight: "WEIGHT", bmi: "BMI", - puls: "PULSE", - schlaf: "SLEEP_DURATION", - stimmung: "MOOD_SCORE", - medikamente: "MEDICATION_COMPLIANCE", + pulse: "PULSE", + sleep: "SLEEP_DURATION", + mood: "MOOD_SCORE", + medications: "MEDICATION_COMPLIANCE", }; interface MetricTargetSummaryProps { - /** Insights category slug, e.g. `"blutdruck"`. */ + /** Insights category slug, e.g. `"blood-pressure"`. */ slug: string; } diff --git a/src/components/insights/therapy-timeline.tsx b/src/components/insights/therapy-timeline.tsx index afcecb39a..4e4a765ab 100644 --- a/src/components/insights/therapy-timeline.tsx +++ b/src/components/insights/therapy-timeline.tsx @@ -11,7 +11,7 @@ import { } from "@/lib/medications/injection-sites"; /** - * v1.4.25 W4d — therapy-timeline view for /insights/medikamente. + * v1.4.25 W4d — therapy-timeline view for /insights/medications. * * Renders a chronological list of GLP-1 therapy events: dose changes, * injections (with site if recorded), inventory events, side-effect tag diff --git a/src/components/insights/vo2-max-chart-row.tsx b/src/components/insights/vo2-max-chart-row.tsx index fc980d154..5b87da9c9 100644 --- a/src/components/insights/vo2-max-chart-row.tsx +++ b/src/components/insights/vo2-max-chart-row.tsx @@ -9,7 +9,7 @@ import { cn } from "@/lib/utils"; import { HealthChartDynamic } from "@/components/charts/health-chart-dynamic"; /** - * v1.4.25 W16a — VO2 max chart-row card for `/insights/puls`. + * v1.4.25 W16a — VO2 max chart-row card for `/insights/pulse`. * * The dashboard already carries an opt-in VO2 max trend tile (W8d.5); * this is the deeper Insights surface. Renders one HealthChart for the diff --git a/src/lib/dashboard-layout.ts b/src/lib/dashboard-layout.ts index ab7afc3b9..d393bbfdd 100644 --- a/src/lib/dashboard-layout.ts +++ b/src/lib/dashboard-layout.ts @@ -176,7 +176,7 @@ export const CHART_OVERLAY_KEYS = [ "medications", "sleep", "steps", - // v1.4.25 W16a — VO2 max chart-row on /insights/puls. Independent + // v1.4.25 W16a — VO2 max chart-row on /insights/pulse. Independent // overlay-prefs slot so the cardio sub-page's VO2 chart-cog state // doesn't bleed into the pulse chart sitting directly above it. "vo2Max", diff --git a/src/lib/feature-flags/__tests__/coach-cascade.test.tsx b/src/lib/feature-flags/__tests__/coach-cascade.test.tsx index 0238743c8..75f067465 100644 --- a/src/lib/feature-flags/__tests__/coach-cascade.test.tsx +++ b/src/lib/feature-flags/__tests__/coach-cascade.test.tsx @@ -261,7 +261,7 @@ describe("Coach disable cascade invariant", () => { // directly. Cross-cut gates on sub-pages (`/targets` page drawer, // `` per-card CTA) sit OUTSIDE the fixture and are owned // by sibling invariants. A future contributor who lands a new Coach - // mount on (e.g.) `/insights/blutdruck/page.tsx` would not trip the + // mount on (e.g.) `/insights/blood-pressure/page.tsx` would not trip the // surface-count check at all — the new site is on a sub-page and // would silently leak. // diff --git a/src/lib/insights/coach-launch-context.tsx b/src/lib/insights/coach-launch-context.tsx index 2b0aacdae..1adfa9824 100644 --- a/src/lib/insights/coach-launch-context.tsx +++ b/src/lib/insights/coach-launch-context.tsx @@ -16,7 +16,7 @@ import type { CoachScopeSource } from "@/lib/ai/coach/types"; * * Until v1.4.27 the Coach drawer was mounted inline on * `/insights/page.tsx` body, which meant navigating to a routed - * sub-page (`/insights/blutdruck`, etc.) unmounted the drawer and + * sub-page (`/insights/blood-pressure`, etc.) unmounted the drawer and * removed every entry point to the Coach. The MA3 audit + Decision F * promoted the drawer up to `/insights/layout.tsx` so every routed * Insights surface can launch it. diff --git a/src/lib/insights/sub-page-metric.ts b/src/lib/insights/sub-page-metric.ts index 3709eb1c2..8b9a2be53 100644 --- a/src/lib/insights/sub-page-metric.ts +++ b/src/lib/insights/sub-page-metric.ts @@ -5,19 +5,24 @@ * trends + advisor). Each named slug below is a sub-route that focuses * one metric. Charts on the sub-pages still use the dashboard chart-cog * surface — see `chartKey` strings in `src/lib/dashboard-layout.ts` - * (`CHART_OVERLAY_KEYS`). The slugs themselves are German because the - * product surface is bilingual and German is the primary author voice. + * (`CHART_OVERLAY_KEYS`). * - * `medikamente` carries no `MeasurementType[]` because medication + * v1.8.0 — the slugs are English. They were German through v1.7.x; the + * naming-convention ADR (`docs/adr/0001-insights-naming-convention.md`) + * settled the surface: UI copy stays localised (resolved through `t()` + * keys), but internal identifiers — route slugs, tile ids, query keys — + * are English. Legacy `/insights/` URLs 301-redirect to their + * English target via `next.config.ts` `redirects()`. + * + * `medications` carries no `MeasurementType[]` because medication * compliance is event-driven (`MedicationIntakeEvent` rows) rather than * time-series `Measurement` rows; the page fetches that data via the * existing `/api/insights/comprehensive` consumer. * - * `bmi` lists both `WEIGHT` + `BODY_HEIGHT` because BMI is derived - * client-side — `WEIGHT / (heightCm/100)^2` — and the chart sets - * `valueMode="bmi"` on ``. `BODY_HEIGHT` is informational - * only (we don't fetch a height series; the user's height lives on the - * profile). + * `bmi` lists `WEIGHT` because BMI is derived client-side — + * `WEIGHT / (heightCm/100)^2` — and the chart sets `valueMode="bmi"` on + * ``. The user's height lives on the profile, so there is + * no separate height series to fetch. */ /** * Single source of truth for the routed insights sub-pages. The slug @@ -36,66 +41,66 @@ // so the five pills collapse behind one "Vitalwerte" / "Vitals" parent // pill with a popover sub-list. The `group: "vitals"` field below // drives that grouping; the canonical order inside the popover follows -// the strip order. The earlier inline pills (`puls`, `blutdruck`, -// `gewicht`, `bmi`, `schlaf`, `stimmung`, `medikamente`, `workouts`) -// stay flat — they predate v1.4.32 and the maintainer's audit kept -// them as direct entries. +// the strip order. The earlier inline pills (`pulse`, `blood-pressure`, +// `weight`, `bmi`, `sleep`, `mood`, `medications`, `workouts`) stay +// flat — they predate v1.4.32 and the maintainer's audit kept them as +// direct entries. const SUB_PAGE_METRIC = { // ── vitals ── - blutdruck: ["BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA", "PULSE"], - puls: ["PULSE"], - sauerstoff: ["OXYGEN_SATURATION"], - koerpertemperatur: ["BODY_TEMPERATURE"], + "blood-pressure": ["BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA", "PULSE"], + pulse: ["PULSE"], + oxygen: ["OXYGEN_SATURATION"], + "body-temperature": ["BODY_TEMPERATURE"], // v1.7.0 — respiratory rate joins the vitals cluster. - atemfrequenz: ["RESPIRATORY_RATE"], + "respiratory-rate": ["RESPIRATORY_RATE"], // ── body composition ── - gewicht: ["WEIGHT"], + weight: ["WEIGHT"], // BMI is derived from WEIGHT + profile height (no separate series). bmi: ["WEIGHT"], // v1.7.0 — Withings / Apple body-composition tail. - koerperwasser: ["TOTAL_BODY_WATER"], - knochenmasse: ["BONE_MASS"], - "fettfreie-masse": ["FAT_FREE_MASS"], - fettmasse: ["FAT_MASS"], - muskelmasse: ["MUSCLE_MASS"], - viszeralfett: ["VISCERAL_FAT"], - magermasse: ["LEAN_BODY_MASS"], + "body-water": ["TOTAL_BODY_WATER"], + "bone-mass": ["BONE_MASS"], + "fat-free-mass": ["FAT_FREE_MASS"], + "fat-mass": ["FAT_MASS"], + "muscle-mass": ["MUSCLE_MASS"], + "visceral-fat": ["VISCERAL_FAT"], + "lean-body-mass": ["LEAN_BODY_MASS"], // ── activity ── - "aktive-energie": ["ACTIVE_ENERGY_BURNED"], + "active-energy": ["ACTIVE_ENERGY_BURNED"], // v1.4.32 — workouts surface; the page reads `Workout` rows directly // through `useWorkouts()` rather than the `summaries` map, so the // metric list stays empty. workouts: [], // v1.7.0 — activity + Apple-Health Mobility cluster. - stockwerke: ["FLIGHTS_CLIMBED"], - gehstrecke: ["WALKING_RUNNING_DISTANCE"], - gangstabilitaet: ["WALKING_STEADINESS"], - gehpuls: ["WALKING_HEART_RATE_AVERAGE"], - gangasymmetrie: ["WALKING_ASYMMETRY"], - doppelstandphase: ["WALKING_DOUBLE_SUPPORT"], - schrittlaenge: ["WALKING_STEP_LENGTH"], - gehgeschwindigkeit: ["WALKING_SPEED"], + "flights-climbed": ["FLIGHTS_CLIMBED"], + "walking-distance": ["WALKING_RUNNING_DISTANCE"], + "walking-steadiness": ["WALKING_STEADINESS"], + "walking-heart-rate": ["WALKING_HEART_RATE_AVERAGE"], + "walking-asymmetry": ["WALKING_ASYMMETRY"], + "double-support-time": ["WALKING_DOUBLE_SUPPORT"], + "step-length": ["WALKING_STEP_LENGTH"], + "walking-speed": ["WALKING_SPEED"], // ── sleep ── - schlaf: ["SLEEP_DURATION"], + sleep: ["SLEEP_DURATION"], // ── cardiovascular ── - ruhepuls: ["RESTING_HEART_RATE"], + "resting-pulse": ["RESTING_HEART_RATE"], hrv: ["HEART_RATE_VARIABILITY"], // v1.7.0 — Withings cardiovascular risk markers. - pulswellengeschwindigkeit: ["PULSE_WAVE_VELOCITY"], - gefaessalter: ["VASCULAR_AGE"], + "pulse-wave-velocity": ["PULSE_WAVE_VELOCITY"], + "vascular-age": ["VASCULAR_AGE"], // ── hearing (v1.7.0) — audio-exposure cluster ── - laermbelastung: ["AUDIO_EXPOSURE_ENV"], - kopfhoererpegel: ["AUDIO_EXPOSURE_HEADPHONE"], - laermereignisse: ["AUDIO_EXPOSURE_EVENT"], + "environmental-audio": ["AUDIO_EXPOSURE_ENV"], + "headphone-audio": ["AUDIO_EXPOSURE_HEADPHONE"], + "audio-events": ["AUDIO_EXPOSURE_EVENT"], // ── environment (v1.7.0) ── - tageslicht: ["TIME_IN_DAYLIGHT"], + daylight: ["TIME_IN_DAYLIGHT"], // ── metabolic (v1.7.0) ── - blutzucker: ["BLOOD_GLUCOSE"], - hauttemperatur: ["SKIN_TEMPERATURE"], + "blood-glucose": ["BLOOD_GLUCOSE"], + "skin-temperature": ["SKIN_TEMPERATURE"], // ── mood ── - stimmung: ["MOOD"], + mood: ["MOOD"], // ── events (medication adherence is event-driven; no measurement series) ── - medikamente: [], + medications: [], } as const satisfies Record; export type SubPageSlug = keyof typeof SUB_PAGE_METRIC; @@ -112,7 +117,7 @@ export const SUB_PAGE_SLUGS = Object.keys(SUB_PAGE_METRIC) as SubPageSlug[]; */ // v1.7.0 — the new metric clusters each collapse behind a parent pill // so the strip stays scannable as the sub-page count grows. The flat -// pills (puls, blutdruck, gewicht, bmi, schlaf, stimmung, medikamente, +// pills (pulse, blood-pressure, weight, bmi, sleep, mood, medications, // workouts) stay direct entries as before. export type SubPageGroup = | "vitals" @@ -126,40 +131,40 @@ export type SubPageGroup = export const SUB_PAGE_GROUP: Partial> = { // vitals hrv: "vitals", - ruhepuls: "vitals", - sauerstoff: "vitals", - koerpertemperatur: "vitals", - "aktive-energie": "vitals", - atemfrequenz: "vitals", + "resting-pulse": "vitals", + oxygen: "vitals", + "body-temperature": "vitals", + "active-energy": "vitals", + "respiratory-rate": "vitals", // body composition (v1.7.0) - koerperwasser: "body", - knochenmasse: "body", - "fettfreie-masse": "body", - fettmasse: "body", - muskelmasse: "body", - viszeralfett: "body", - magermasse: "body", + "body-water": "body", + "bone-mass": "body", + "fat-free-mass": "body", + "fat-mass": "body", + "muscle-mass": "body", + "visceral-fat": "body", + "lean-body-mass": "body", // activity / mobility (v1.7.0) - stockwerke: "activity", - gehstrecke: "activity", - gangstabilitaet: "activity", - gehpuls: "activity", - gangasymmetrie: "activity", - doppelstandphase: "activity", - schrittlaenge: "activity", - gehgeschwindigkeit: "activity", + "flights-climbed": "activity", + "walking-distance": "activity", + "walking-steadiness": "activity", + "walking-heart-rate": "activity", + "walking-asymmetry": "activity", + "double-support-time": "activity", + "step-length": "activity", + "walking-speed": "activity", // cardiovascular (v1.7.0) - pulswellengeschwindigkeit: "cardiovascular", - gefaessalter: "cardiovascular", + "pulse-wave-velocity": "cardiovascular", + "vascular-age": "cardiovascular", // hearing (v1.7.0) - laermbelastung: "hearing", - kopfhoererpegel: "hearing", - laermereignisse: "hearing", + "environmental-audio": "hearing", + "headphone-audio": "hearing", + "audio-events": "hearing", // environment (v1.7.0) - tageslicht: "environment", + daylight: "environment", // metabolic (v1.7.0) - blutzucker: "metabolic", - hauttemperatur: "metabolic", + "blood-glucose": "metabolic", + "skin-temperature": "metabolic", }; /**