mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
refactor(insights): migrate route slugs to english with redirects
Rename every routed Insights sub-page slug from German to English (`/insights/blutdruck` -> `/insights/blood-pressure`, `gewicht` -> `weight`, `medikamente` -> `medications`, and the full v1.7.0 metric tail). UI copy stays localised through `t()` keys; only the internal slugs change. The slug registry in `src/lib/insights/sub-page-metric.ts` is the single source of truth, so the tab strip, target summary, and daily-briefing routing targets follow it. `next.config.ts` `redirects()` emits a permanent redirect from every legacy German slug to its English target, including a `:path*` variant so nested routes survive. Bookmarks and cached PWA navigation keep resolving.
This commit is contained in:
1 parent
b416cabbf1
commit
c4ccd7e297
46 files changed
+244
-180
No files matched your search
@@ -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 [
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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-
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
<MetricTargetSummary slug="blutdruck" />
|
||||
<MetricTargetSummary slug="blood-pressure" />
|
||||
|
||||
<InsightStatusCard
|
||||
title={t("insights.assessmentTitle")}
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { Thermometer } from "lucide-react";
|
||||
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
|
||||
|
||||
/**
|
||||
* v1.4.32 — `/insights/koerpertemperatur`.
|
||||
* v1.4.32 — `/insights/body-temperature`.
|
||||
*
|
||||
* Body-temperature sub-page. Canonical storage is Celsius. Distinct
|
||||
* from skin temperature (Withings ScanWatch dermal reading) — surface
|
||||
@@ -5,7 +5,7 @@ import { Droplet } from "lucide-react";
|
||||
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
|
||||
|
||||
/**
|
||||
* v1.7.0 — `/insights/koerperwasser`.
|
||||
* v1.7.0 — `/insights/body-water`.
|
||||
*
|
||||
* TOTAL_BODY_WATER sub-page. Reuses the generic HealthKitMetricPage scaffold;
|
||||
* the chart daily-aggregates through the rollup read path. Empty-state
|
||||
@@ -5,7 +5,7 @@ import { Bone } from "lucide-react";
|
||||
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
|
||||
|
||||
/**
|
||||
* v1.7.0 — `/insights/knochenmasse`.
|
||||
* v1.7.0 — `/insights/bone-mass`.
|
||||
*
|
||||
* BONE_MASS sub-page. Reuses the generic HealthKitMetricPage scaffold;
|
||||
* the chart daily-aggregates through the rollup read path. Empty-state
|
||||
@@ -5,7 +5,7 @@ import { Sun } from "lucide-react";
|
||||
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
|
||||
|
||||
/**
|
||||
* v1.7.0 — `/insights/tageslicht`.
|
||||
* v1.7.0 — `/insights/daylight`.
|
||||
*
|
||||
* TIME_IN_DAYLIGHT sub-page. Reuses the generic HealthKitMetricPage scaffold;
|
||||
* the chart daily-aggregates through the rollup read path. Empty-state
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { Footprints } from "lucide-react";
|
||||
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
|
||||
|
||||
/**
|
||||
* v1.7.0 — `/insights/doppelstandphase`.
|
||||
* v1.7.0 — `/insights/double-support-time`.
|
||||
*
|
||||
* WALKING_DOUBLE_SUPPORT sub-page. Reuses the generic HealthKitMetricPage scaffold;
|
||||
* the chart daily-aggregates through the rollup read path. Empty-state
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { Volume2 } from "lucide-react";
|
||||
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
|
||||
|
||||
/**
|
||||
* v1.7.0 — `/insights/laermbelastung`.
|
||||
* v1.7.0 — `/insights/environmental-audio`.
|
||||
*
|
||||
* AUDIO_EXPOSURE_ENV sub-page. Reuses the generic HealthKitMetricPage scaffold;
|
||||
* the chart daily-aggregates through the rollup read path. Empty-state
|
||||
@@ -5,7 +5,7 @@ import { Scale } from "lucide-react";
|
||||
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
|
||||
|
||||
/**
|
||||
* v1.7.0 — `/insights/fettfreie-masse`.
|
||||
* v1.7.0 — `/insights/fat-free-mass`.
|
||||
*
|
||||
* FAT_FREE_MASS sub-page. Reuses the generic HealthKitMetricPage scaffold;
|
||||
* the chart daily-aggregates through the rollup read path. Empty-state
|
||||
@@ -5,7 +5,7 @@ import { Droplets } from "lucide-react";
|
||||
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
|
||||
|
||||
/**
|
||||
* v1.7.0 — `/insights/fettmasse`.
|
||||
* v1.7.0 — `/insights/fat-mass`.
|
||||
*
|
||||
* FAT_MASS sub-page. Reuses the generic HealthKitMetricPage scaffold;
|
||||
* the chart daily-aggregates through the rollup read path. Empty-state
|
||||
@@ -5,7 +5,7 @@ import { TrendingUp } from "lucide-react";
|
||||
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
|
||||
|
||||
/**
|
||||
* v1.7.0 — `/insights/stockwerke`.
|
||||
* v1.7.0 — `/insights/flights-climbed`.
|
||||
*
|
||||
* FLIGHTS_CLIMBED sub-page. Reuses the generic HealthKitMetricPage scaffold;
|
||||
* the chart daily-aggregates through the rollup read path. Empty-state
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { Headphones } from "lucide-react";
|
||||
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
|
||||
|
||||
/**
|
||||
* v1.7.0 — `/insights/kopfhoererpegel`.
|
||||
* v1.7.0 — `/insights/headphone-audio`.
|
||||
*
|
||||
* AUDIO_EXPOSURE_HEADPHONE sub-page. Reuses the generic HealthKitMetricPage scaffold;
|
||||
* the chart daily-aggregates through the rollup read path. Empty-state
|
||||
@@ -5,7 +5,7 @@ import { Scale } from "lucide-react";
|
||||
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
|
||||
|
||||
/**
|
||||
* v1.7.0 — `/insights/magermasse`.
|
||||
* v1.7.0 — `/insights/lean-body-mass`.
|
||||
*
|
||||
* LEAN_BODY_MASS sub-page. Reuses the generic HealthKitMetricPage scaffold;
|
||||
* the chart daily-aggregates through the rollup read path. Empty-state
|
||||
@@ -21,7 +21,7 @@ import { TherapyTimeline } from "@/components/insights/therapy-timeline";
|
||||
import { MedicationCardHeader } from "@/components/medications/MedicationCardHeader";
|
||||
|
||||
/**
|
||||
* v1.4.25 W4 — `/insights/medikamente`.
|
||||
* v1.4.25 W4 — `/insights/medications`.
|
||||
*
|
||||
* Routed Medication-Compliance sub-page. Mirrors the per-medication
|
||||
* grid from the v1.4.24 mother page (compliance bars + heatmap +
|
||||
@@ -166,7 +166,7 @@ export default function InsightsMedikamentePage() {
|
||||
{medications.map((med) => {
|
||||
const medicationSummary = medicationSummaryById.get(med.id);
|
||||
// UI-H3 — route every medication-list row (medications page
|
||||
// + insights/medikamente) through `<MedicationCardHeader>`
|
||||
// + insights/medications) through `<MedicationCardHeader>`
|
||||
// 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() {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<MetricTargetSummary slug="medikamente" />
|
||||
<MetricTargetSummary slug="medications" />
|
||||
|
||||
<InsightStatusCard
|
||||
title={t("insights.assessmentTitle")}
|
||||
@@ -19,7 +19,7 @@ import { MetricTargetSummary } from "@/components/insights/metric-target-summary
|
||||
import { SubPageShell } from "@/components/insights/sub-page-shell";
|
||||
|
||||
/**
|
||||
* v1.4.25 W4 — `/insights/stimmung`.
|
||||
* v1.4.25 W4 — `/insights/mood`.
|
||||
*
|
||||
* Routed Mood sub-page. Unlike the mother page (which hid the mood
|
||||
* section entirely when no mood data existed), a dedicated sub-page
|
||||
@@ -105,7 +105,7 @@ export default function InsightsStimmungPage() {
|
||||
userTimezone={user?.timezone}
|
||||
/>
|
||||
|
||||
<MetricTargetSummary slug="stimmung" />
|
||||
<MetricTargetSummary slug="mood" />
|
||||
|
||||
<InsightStatusCard
|
||||
title={t("insights.assessmentTitle")}
|
||||
@@ -5,7 +5,7 @@ import { Dumbbell } from "lucide-react";
|
||||
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
|
||||
|
||||
/**
|
||||
* v1.7.0 — `/insights/muskelmasse`.
|
||||
* v1.7.0 — `/insights/muscle-mass`.
|
||||
*
|
||||
* MUSCLE_MASS sub-page. Reuses the generic HealthKitMetricPage scaffold;
|
||||
* the chart daily-aggregates through the rollup read path. Empty-state
|
||||
@@ -5,7 +5,7 @@ import { Wind } from "lucide-react";
|
||||
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
|
||||
|
||||
/**
|
||||
* v1.4.32 — `/insights/sauerstoff`.
|
||||
* v1.4.32 — `/insights/oxygen`.
|
||||
*
|
||||
* Oxygen-saturation (SpO₂) sub-page. Canonical storage is percent
|
||||
* (0..100); the chart axis runs 85..100 because clinical normal is
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { Activity } from "lucide-react";
|
||||
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
|
||||
|
||||
/**
|
||||
* v1.7.0 — `/insights/pulswellengeschwindigkeit`.
|
||||
* v1.7.0 — `/insights/pulse-wave-velocity`.
|
||||
*
|
||||
* PULSE_WAVE_VELOCITY sub-page. Reuses the generic HealthKitMetricPage scaffold;
|
||||
* the chart daily-aggregates through the rollup read path. Empty-state
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from "@/lib/analytics/pulse-targets";
|
||||
|
||||
/**
|
||||
* v1.4.25 W4 — `/insights/puls`.
|
||||
* v1.4.25 W4 — `/insights/pulse`.
|
||||
*
|
||||
* Routed Pulse sub-page. Renders the pulse chart with the personalized
|
||||
* Karvonen-derived target band plus the per-section AI assessment.
|
||||
@@ -126,7 +126,7 @@ export default function InsightsPulsPage() {
|
||||
userTimezone={user?.timezone}
|
||||
/>
|
||||
|
||||
<MetricTargetSummary slug="puls" />
|
||||
<MetricTargetSummary slug="pulse" />
|
||||
|
||||
<InsightStatusCard
|
||||
title={t("insights.assessmentTitle")}
|
||||
@@ -5,7 +5,7 @@ import { Wind } from "lucide-react";
|
||||
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
|
||||
|
||||
/**
|
||||
* v1.7.0 — `/insights/atemfrequenz`.
|
||||
* v1.7.0 — `/insights/respiratory-rate`.
|
||||
*
|
||||
* RESPIRATORY_RATE sub-page. Reuses the generic HealthKitMetricPage scaffold;
|
||||
* the chart daily-aggregates through the rollup read path. Empty-state
|
||||
@@ -5,9 +5,9 @@ import { Heart } from "lucide-react";
|
||||
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
|
||||
|
||||
/**
|
||||
* v1.4.32 — `/insights/ruhepuls`.
|
||||
* v1.4.32 — `/insights/resting-pulse`.
|
||||
*
|
||||
* Resting-heart-rate sub-page. Distinct from `/insights/puls` — Apple
|
||||
* Resting-heart-rate sub-page. Distinct from `/insights/pulse` — Apple
|
||||
* Health splits the steady-state daily `restingHeartRate` value from
|
||||
* the spot `PULSE` samples logged manually or by Withings. The split
|
||||
* here mirrors that distinction so the user has a "daily floor"
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { Thermometer } from "lucide-react";
|
||||
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
|
||||
|
||||
/**
|
||||
* v1.7.0 — `/insights/hauttemperatur`.
|
||||
* v1.7.0 — `/insights/skin-temperature`.
|
||||
*
|
||||
* SKIN_TEMPERATURE sub-page. Reuses the generic HealthKitMetricPage scaffold;
|
||||
* the chart daily-aggregates through the rollup read path. Empty-state
|
||||
@@ -13,7 +13,7 @@ import { SleepOverview } from "@/components/insights/sleep-overview";
|
||||
import { SubPageShell } from "@/components/insights/sub-page-shell";
|
||||
|
||||
/**
|
||||
* v1.4.25 W4c — `/insights/schlaf`.
|
||||
* v1.4.25 W4c — `/insights/sleep`.
|
||||
*
|
||||
* The Sleep sub-page surfaces the per-stage breakdown + duration trend
|
||||
* the v1.4.23 schema gained but never rendered. All charts live inside
|
||||
@@ -65,7 +65,7 @@ export default function InsightsSchlafPage() {
|
||||
>
|
||||
<SleepOverview />
|
||||
|
||||
<MetricTargetSummary slug="schlaf" />
|
||||
<MetricTargetSummary slug="sleep" />
|
||||
|
||||
{/*
|
||||
v1.4.28 (BK-UI-StatusSchlaf) — the six sibling sub-pages all
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
+1
-1
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
+1
-1
@@ -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
|
||||
+1
-1
@@ -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
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
<MetricTargetSummary slug="gewicht" />
|
||||
<MetricTargetSummary slug="weight" />
|
||||
|
||||
<InsightStatusCard
|
||||
title={t("insights.assessmentTitle")}
|
||||
@@ -80,7 +80,7 @@ const BP_DATA: TargetsFixture = {
|
||||
|
||||
describe("<MetricTargetSummary>", () => {
|
||||
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("<MetricTargetSummary>", () => {
|
||||
});
|
||||
|
||||
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("<MetricTargetSummary>", () => {
|
||||
});
|
||||
|
||||
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 }],
|
||||
});
|
||||
|
||||
@@ -39,23 +39,23 @@ import type {
|
||||
* through to `null` and render as a plain row (no link wrap).
|
||||
*/
|
||||
const METRIC_HREF: Record<DailyBriefingKeyFinding["sourceMetric"], string | null> = {
|
||||
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",
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 `<Link>` to a routed sub-page (`/insights/blutdruck` …),
|
||||
* is now a `<Link>` 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
|
||||
|
||||
@@ -67,17 +67,17 @@ interface TargetsResponse {
|
||||
* component renders nothing for them, so the sub-page collapses cleanly.
|
||||
*/
|
||||
const SLUG_TO_TARGET_TYPE: Record<string, string> = {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -261,7 +261,7 @@ describe("Coach disable cascade invariant", () => {
|
||||
// directly. Cross-cut gates on sub-pages (`/targets` page drawer,
|
||||
// `<TargetCard>` 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.
|
||||
//
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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/<de-slug>` 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 `<HealthChart>`. `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
|
||||
* `<HealthChart>`. 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<string, readonly string[]>;
|
||||
|
||||
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<Record<SubPageSlug, SubPageGroup>> = {
|
||||
// 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",
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user