mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
feat(dashboard): VO2 max trend tile (opt-in, secondary-metric pattern)
Adds the VO2 max strip tile to /, matching the existing
secondary-metric convention (sleep / steps / glucose / oxygen
saturation are all default-invisible + opt-in via Settings →
Dashboard).
Wire-up:
- src/lib/dashboard-layout.ts: vo2Max appended to
DASHBOARD_WIDGET_IDS and DEFAULT_DASHBOARD_LAYOUT.widgets with
visible=false / tileVisible=false. The /api/dashboard/widgets
Zod enum is derived from the same array so the toggle is wired
automatically.
- src/app/page.tsx: vo2Summary from data?.summaries?.VO2_MAX
(auto-populated by /api/analytics; the route iterates the full
measurementTypeEnum.options list, no backend change needed),
hasVo2 data-floor gate, showVo2Tile combined gate, and the
TrendCard push next to showStepsTile. directionSentiment is
up-good (higher VO2 max is better) and the unit string runs
through the t() helper so the iOS app session can localise it.
- src/components/settings/dashboard-layout-section.tsx: vo2Max
label key added to WIDGET_LABEL_KEYS so the Settings → Dashboard
toggle row renders the localized label.
- messages/{en,de,fr,es,it,pl}.json: dashboard.vo2Max + vo2MaxUnit
keys. "VO₂ max" is universal across all six locales (scientific
abbreviation, no translation needed); the unit string is
consistent SI notation.
No chart-row card — that's an explicit v1.5 deferral per the W8d
outline §6.4. The tile lights up the moment the user accumulates any
VO2 sample (Withings ingest or the upcoming iOS Health passthrough).
This commit is contained in:
1 parent
8f57f5e0d7
commit
b324e6c41d
9 files changed
+61
No files matched your search
@@ -247,6 +247,8 @@
|
||||
"bodyFat": "Körperfett",
|
||||
"sleep": "Schlaf",
|
||||
"steps": "Schritte",
|
||||
"vo2Max": "VO₂ max",
|
||||
"vo2MaxUnit": "mL/(kg·min)",
|
||||
"bpInTarget": "BD im Zielbereich",
|
||||
"days30Short": "30T",
|
||||
"medications": "Medikamente",
|
||||
|
||||
@@ -247,6 +247,8 @@
|
||||
"bodyFat": "Body Fat",
|
||||
"sleep": "Sleep",
|
||||
"steps": "Steps",
|
||||
"vo2Max": "VO₂ max",
|
||||
"vo2MaxUnit": "mL/(kg·min)",
|
||||
"bpInTarget": "BP in Target",
|
||||
"days30Short": "30d",
|
||||
"medications": "Medications",
|
||||
|
||||
@@ -247,6 +247,8 @@
|
||||
"bodyFat": "Grasa corporal",
|
||||
"sleep": "Sueño",
|
||||
"steps": "Pasos",
|
||||
"vo2Max": "VO₂ max",
|
||||
"vo2MaxUnit": "mL/(kg·min)",
|
||||
"bpInTarget": "BP in Target",
|
||||
"days30Short": "30d",
|
||||
"medications": "Medicamentos",
|
||||
|
||||
@@ -247,6 +247,8 @@
|
||||
"bodyFat": "Masse grasse",
|
||||
"sleep": "Sommeil",
|
||||
"steps": "Pas",
|
||||
"vo2Max": "VO₂ max",
|
||||
"vo2MaxUnit": "mL/(kg·min)",
|
||||
"bpInTarget": "BP in Target",
|
||||
"days30Short": "30d",
|
||||
"medications": "Médicaments",
|
||||
|
||||
@@ -247,6 +247,8 @@
|
||||
"bodyFat": "Massa grassa",
|
||||
"sleep": "Sonno",
|
||||
"steps": "Passi",
|
||||
"vo2Max": "VO₂ max",
|
||||
"vo2MaxUnit": "mL/(kg·min)",
|
||||
"bpInTarget": "BP in Target",
|
||||
"days30Short": "30d",
|
||||
"medications": "Farmaci",
|
||||
|
||||
@@ -247,6 +247,8 @@
|
||||
"bodyFat": "Tkanka tłuszczowa",
|
||||
"sleep": "Sen",
|
||||
"steps": "Kroki",
|
||||
"vo2Max": "VO₂ max",
|
||||
"vo2MaxUnit": "mL/(kg·min)",
|
||||
"bpInTarget": "BP in Target",
|
||||
"days30Short": "30d",
|
||||
"medications": "Leki",
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Activity,
|
||||
Droplet,
|
||||
Footprints,
|
||||
Gauge,
|
||||
Heart,
|
||||
Moon,
|
||||
Percent,
|
||||
@@ -251,6 +252,10 @@ export default function DashboardPage() {
|
||||
const bf = data?.summaries?.BODY_FAT;
|
||||
const sleepSummary = data?.summaries?.SLEEP_DURATION;
|
||||
const stepsSummary = data?.summaries?.ACTIVITY_STEPS;
|
||||
// v1.4.25 W8d — VO2 max secondary-metric tile. /api/analytics
|
||||
// auto-populates this summary because the route iterates over the
|
||||
// full measurementTypeEnum.options list; no backend change needed.
|
||||
const vo2Summary = data?.summaries?.VO2_MAX;
|
||||
const moodSummary = moodData?.summary;
|
||||
|
||||
// Resolve full dashboard layout — controls visibility + order of every widget
|
||||
@@ -307,6 +312,7 @@ export default function DashboardPage() {
|
||||
const hasMood = (moodSummary?.count ?? 0) > 0;
|
||||
const hasSleep = (sleepSummary?.count ?? 0) > 0;
|
||||
const hasSteps = (stepsSummary?.count ?? 0) > 0;
|
||||
const hasVo2 = (vo2Summary?.count ?? 0) > 0;
|
||||
const hasBpInTarget = data?.bpInTargetPct != null;
|
||||
|
||||
// Tile (strip) gates — controlled by the new `tileVisible` flag.
|
||||
@@ -317,6 +323,7 @@ export default function DashboardPage() {
|
||||
const showMoodTile = isTileVisible("mood") && hasMood;
|
||||
const showSleepTile = isTileVisible("sleep") && hasSleep;
|
||||
const showStepsTile = isTileVisible("steps") && hasSteps;
|
||||
const showVo2Tile = isTileVisible("vo2Max") && hasVo2;
|
||||
const showBpInTargetTile = isTileVisible("bpInTarget") && hasBpInTarget;
|
||||
|
||||
// Chart (lower row) gates — controlled by the legacy `visible` flag.
|
||||
@@ -809,6 +816,35 @@ export default function DashboardPage() {
|
||||
),
|
||||
});
|
||||
}
|
||||
// v1.4.25 W8d — VO2 max trend tile. Self-gates on the
|
||||
// `vo2Max` widget being enabled (Settings → Dashboard) AND
|
||||
// the analytics summary carrying at least one sample. Higher
|
||||
// VO2 max is better, so the directionSentiment is up-good and
|
||||
// an upward 30-day slope renders the green arrow. Unit
|
||||
// matches the canonical DB unit in
|
||||
// src/lib/validations/measurement.ts.
|
||||
if (showVo2Tile) {
|
||||
trendCards.push({
|
||||
id: "vo2Max",
|
||||
order: widgetOrder("vo2Max"),
|
||||
node: (
|
||||
<TrendCard
|
||||
key="vo2Max"
|
||||
label={t("dashboard.vo2Max") ?? "VO₂ max"}
|
||||
latest={vo2Summary?.latest ?? null}
|
||||
unit={t("dashboard.vo2MaxUnit") ?? "mL/(kg·min)"}
|
||||
avg7={vo2Summary?.avg7 ?? null}
|
||||
avg30={vo2Summary?.avg30 ?? null}
|
||||
slope30={vo2Summary?.slope30 ?? null}
|
||||
trend7Delta={summaryToTrend7Delta(vo2Summary)}
|
||||
icon={Gauge}
|
||||
directionSentiment="up-good"
|
||||
compareBaseline={compareBaseline}
|
||||
compareDelta={tileCompareDelta(vo2Summary)}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
if (showBpInTargetTile) {
|
||||
/* v1.4.22 A2 — feature parity with every other tile.
|
||||
Synthesise a slope from the difference between the 7-day
|
||||
|
||||
@@ -48,6 +48,8 @@ const WIDGET_LABEL_KEYS: Record<DashboardWidgetId, string> = {
|
||||
// for the polished `<InsightsCardPreview>` (top severity-ordered AI
|
||||
// recommendations + ring confidence meter + "View all" CTA).
|
||||
insightsPreview: "dashboard.insightsPreview",
|
||||
// v1.4.25 W8d — VO2 max secondary-metric tile (opt-in).
|
||||
vo2Max: "dashboard.vo2Max",
|
||||
};
|
||||
|
||||
export function DashboardLayoutSection({ id }: { id: string }) {
|
||||
|
||||
@@ -34,6 +34,12 @@ export const DASHBOARD_WIDGET_IDS = [
|
||||
// a `visible` (chart-row) surface; tileVisible is forced false because
|
||||
// there's no tile-strip representation for it.
|
||||
"insightsPreview",
|
||||
// v1.4.25 W8d — VO2 max trend tile (opt-in, secondary-metric pattern).
|
||||
// Self-gates on having any VO2_MAX sample; default-invisible so
|
||||
// existing dashboards stay unchanged until the user enables it in
|
||||
// Settings → Dashboard. Only the tile-strip surface; chart row
|
||||
// lands alongside the iOS-app body-composition sub-page in v1.5.
|
||||
"vo2Max",
|
||||
] as const;
|
||||
|
||||
export type DashboardWidgetId = (typeof DASHBOARD_WIDGET_IDS)[number];
|
||||
@@ -217,6 +223,11 @@ export const DEFAULT_DASHBOARD_LAYOUT: DashboardLayout = {
|
||||
// so it stays pinned above charts even if the user reorders other
|
||||
// widgets; only the boolean visibility flag is consumed.
|
||||
{ id: "insightsPreview", visible: true, tileVisible: false, order: 14 },
|
||||
// v1.4.25 W8d — VO2 max trend tile. Default-invisible on both
|
||||
// surfaces; opt-in via Settings → Dashboard. Only the tile-strip
|
||||
// representation today (`visible` stays false so the chart-row
|
||||
// gate never fires; the chart card lands in v1.5).
|
||||
{ id: "vo2Max", visible: false, tileVisible: false, order: 15 },
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user