fix(dashboard): open the body fat page from the body fat tile

The body fat tile was the one strip tile that led nowhere. Its href map
said there was no dedicated sub-page, which was true: body fat percentage
was tiled and charted on the dashboard but never got the Insights page
every body-composition sibling has.

Add /insights/body-fat on the generic metric-page scaffold (body group,
tab-strip pill, seven locales, manual-entry empty state) and link the
tile to it.

Move the tile href map out of the page into tile-destinations.ts, keyed by
the tile-capable widget id and typed against the sub-page slug union, so
a missing or non-existent destination fails to compile. The link passes
the destination's insights-page surface gate, so it never points at a
page a switched-off module hides.

A guard derives the tile set from TILE_CAPABLE_WIDGET_IDS and from the
ids the dashboard actually pushes into its strip, and fails when a tile's
metric has a sub-page the tile does not open, when a destination has no
route, or when a tile links to a page owned by another module. Tiles that
deliberately lead nowhere need a written reason; there are none today.

The reverse-lookup test pinned BODY_FAT as having no page; it now
resolves to body-fat.

Refs #1084
This commit is contained in:
Marc-André Bombeck committed 2026-10-02 21:21:38 +02:00
1 parent 2ea9dc3936
commit 885efaafd9
17 files changed
+440 -48

No files matched your search

+31
View File
@@ -0,0 +1,31 @@
"use client";
import { Percent } from "lucide-react";
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
/**
* `/insights/body-fat`.
*
* BODY_FAT sub-page on the generic HealthKitMetricPage scaffold, like its
* body-composition siblings. The dashboard tile and chart for body fat
* predate the sub-pages; this is the page the tile opens. Readings arrive
* from a body-composition scale or by hand, so the empty state offers
* manual entry.
*/
export default function InsightsBodyFatPage() {
return (
<HealthKitMetricPage
measurementType="BODY_FAT"
insightMetric="BODY_FAT"
chartKey="bodyFat"
i18nPrefix="insights.bodyFat"
explainerMetric="bodyFat"
color="var(--warning)"
unit="%"
statIcon={Percent}
emptyStateIcon={<Percent className="size-6" />}
emptyStateCtaType="BODY_FAT"
/>
);
}
+5 -39
View File
@@ -174,6 +174,7 @@ export {
resolveConfiguredTileCount,
resolveChartRowPlaceholderCount,
} from "@/components/dashboard/dashboard-gates";
import { dashboardTileHref } from "@/components/dashboard/tile-destinations";
import {
resolveDashboardFirstPaintGate,
resolveConfiguredTileCount,
@@ -182,42 +183,6 @@ import {
resolveGlucoseTiles,
} from "@/components/dashboard/dashboard-gates";
/**
* Dashboard metric tiles are no longer dead-ends: each tile links to its
* Insights detail page (bands, correlations, history). Keyed by the stable
* tile `id` used in the `trendCards` builder; glucose contexts share one
* blood-glucose destination via the `glucose-` prefix. Tiles with no
* dedicated sub-page (body fat %) stay un-linked rather than misroute.
*/
const TILE_INSIGHT_HREF: Record<string, string> = {
weight: "/insights/weight",
"bp-sys": "/insights/blood-pressure",
"bp-dia": "/insights/blood-pressure",
bpInTarget: "/insights/blood-pressure",
pulse: "/insights/pulse",
mood: "/insights/mood",
sleep: "/insights/sleep",
steps: "/insights/steps",
vo2Max: "/insights/cardio-fitness",
// v1.28.52 — vitals + body-composition strip tiles link to their
// existing /insights detail pages (bands, history, correlations).
hrv: "/insights/hrv",
oxygenSaturation: "/insights/oxygen",
respiratoryRate: "/insights/respiratory-rate",
wristTemperature: "/insights/wrist-temperature",
muscleMass: "/insights/muscle-mass",
totalBodyWater: "/insights/body-water",
boneMass: "/insights/bone-mass",
// v1.29 — fluid intake links to the nutrients surface (hydration hero +
// quick-add), not a Measurement-backed detail page.
waterIntake: "/insights/nutrients",
};
function tileInsightHref(id: string): string | null {
if (id.startsWith("glucose-")) return "/insights/blood-glucose";
return TILE_INSIGHT_HREF[id] ?? null;
}
export default function DashboardPageClient({
batchWindow: batchWindowProp,
}: {
@@ -2172,9 +2137,10 @@ export default function DashboardPageClient({
</Suspense>
);
// A tile is no longer a dead-end: it links to its Insights
// detail page. The link fills the grid cell and paints a
// ring on hover / focus so the whole tile reads as tappable.
const href = tileInsightHref(entry.id);
// detail page (`tile-destinations.ts`, gated on the page's
// module). The link fills the grid cell and paints a ring
// on hover / focus so the whole tile reads as tappable.
const href = dashboardTileHref(entry.id, user?.modules);
return href ? (
<Link
key={entry.id}