+ {/* v1.15.14 — bounded compact height (was `aspect-[3/2] min-h-[160px]`,
+ which derived its height off the full card width and ballooned on a
+ wide viewport). A fixed band keeps it a tidy card matching the
+ distribution sibling. */}
+
-
+ {/* v1.15.14 — calmer target guide: a muted comment-grey dashed
+ line instead of the bright green, so it reads as a quiet
+ reference rather than competing with the bars. */}
+
-
+ {/* v1.15.14 — soften the saturated mood hues to a muted tint
+ (the level semantics still distinguish the bars; the fill no
+ longer shouts). Scoped here — the shared `--dracula-*` mood
+ tokens used by the heatmap/legend stay untouched. */}
+
{data.map((row) => (
| {
expect(resolveTileLayout(DEFAULT_INSIGHTS_LAYOUT, id).visible).toBe(true);
}
});
+
+ // v1.15.14 — the default is ALL-VISIBLE. The v1.15.11 curated subset
+ // dropped ~20 nav pills once the tab strip began gating on the layout
+ // (sleep, steps, active-energy, walking-*, audio-*, daylight, the body-
+ // composition tiles, …). The grid renders a fixed mapped subset
+ // regardless of `tiles.visible`, so all-visible does NOT bloat it; what
+ // it restores is the nav's everything-with-data default. Pin it.
+ it("defaults EVERY tile visible (nav-pill regression guard)", () => {
+ expect(DEFAULT_INSIGHTS_LAYOUT.tiles.length).toBe(INSIGHTS_TILE_IDS.length);
+ for (const tile of DEFAULT_INSIGHTS_LAYOUT.tiles) {
+ expect(tile.visible).toBe(true);
+ }
+ // Spot-check the long-tail slugs that previously regressed out of nav.
+ for (const id of [
+ "sleep",
+ "steps",
+ "active-energy",
+ "walking-distance",
+ "walking-speed",
+ "flights-climbed",
+ "environmental-audio",
+ "headphone-audio",
+ "daylight",
+ "breathing-disturbances",
+ "skin-temperature",
+ "body-water",
+ "fat-mass",
+ "muscle-mass",
+ "lean-body-mass",
+ ]) {
+ expect(resolveTileLayout(DEFAULT_INSIGHTS_LAYOUT, id).visible).toBe(true);
+ }
+ });
});
diff --git a/src/lib/insights-layout.ts b/src/lib/insights-layout.ts
index 3a8eb7bc6..501136496 100644
--- a/src/lib/insights-layout.ts
+++ b/src/lib/insights-layout.ts
@@ -140,55 +140,30 @@ const INSIGHTS_LAYOUT_VERSION = 2;
/**
* Default layout mirrors the routed tab-strip order — vitals first,
* then body composition, activity, sleep, cardiovascular, mood, then
- * events. Core surfaces are default-visible; the long-tail HealthKit
- * pills (oxygen saturation, body temperature, active energy, sleep,
- * resting HR, HRV) stay default-invisible and the user opts in once
- * the matching data starts to flow. Adding a new tile lands as an
- * additional `{ id, visible: false, order: N }` row here and the
- * resolver auto-merges it onto existing users' saved layouts.
+ * events. Every tile ships default-VISIBLE; the user hides what they
+ * don't want. Adding a new tile lands as an additional default-visible
+ * row here and the resolver auto-merges it onto existing users' saved
+ * layouts.
+ *
+ * v1.15.14 — the default is now ALL-VISIBLE. The v1.15.11 design shipped
+ * a curated subset (`DEFAULT_VISIBLE_TILE_IDS`) authored for the overview
+ * Vitals/Mobility GRID, which renders only a FIXED mapped set of tiles
+ * (the four derived re-frames + `SECTION_VITALS` + `SECTION_MOBILITY` —
+ * see `vitals-dashboard.tsx`). But once the tab strip began gating its
+ * nav pills on the same layout (v1.15.14 W2), that curated subset dropped
+ * ~20 nav pills for a fresh account with data in Sleep, Steps,
+ * Active-Energy, body composition, Walking/mobility, Audio, Daylight,
+ * etc. — pills that show today — and caused a "pills flash in then
+ * vanish" once the layout GET settled.
+ *
+ * The grid renders a fixed mapped subset regardless of `tiles.visible`
+ * (the long-tail slugs never appear in the grid), so defaulting EVERY
+ * tile visible does NOT bloat the grid — it stays its curated, data-gated
+ * set. What it restores is the tab strip's everything-with-data default:
+ * a fresh account sees every pill it has data for, and the user hides the
+ * ones they don't want. Data availability stays the floor everywhere — a
+ * visible tile with no readings still renders nothing.
*/
-// Core surfaces that ship default-visible for a fresh account. Every
-// other tile (the long-tail HealthKit + body-composition + mobility +
-// audio pills) stays default-invisible and the user opts in once the
-// matching data starts to flow. Keeping this as an explicit set means
-// the default-visibility decision is auditable in one place while the
-// tile-id universe itself derives from `SUB_PAGE_SLUGS`.
-const DEFAULT_VISIBLE_TILE_IDS = new Set([
- "overview",
- "blood-pressure",
- "pulse",
- "weight",
- "bmi",
- "workouts",
- "mood",
- "medications",
- // v1.15.11 W2c — the Vitals + Mobility grid on the overview surfaces every
- // one of these tiles today, gated only on data availability. Now that the
- // grid honours this layout's tile visibility, they must ship default-visible
- // or a fresh account (no saved layout) would lose the tiles it shows today —
- // a regression. They stay data-gated on top of visibility, so a tile with no
- // readings still renders nothing exactly as before; widening the default set
- // only ensures the layout never *suppresses* a tile the user already sees.
- // The ids map to the grid's tile concepts:
- // cardio-fitness → FitnessAge (VO₂max re-frame); vascular-age →
- // VascularAge; hrv → HRV balance; resting-pulse / respiratory-rate /
- // oxygen / body-temperature / blood-glucose → the per-vital baseline
- // tiles; six-minute-walk / stair-ascent-speed / stair-descent-speed /
- // wrist-temperature → the Mobility sub-grid.
- "cardio-fitness",
- "vascular-age",
- "hrv",
- "resting-pulse",
- "respiratory-rate",
- "oxygen",
- "body-temperature",
- "blood-glucose",
- "six-minute-walk",
- "stair-ascent-speed",
- "stair-descent-speed",
- "wrist-temperature",
-]);
-
export const DEFAULT_INSIGHTS_LAYOUT: InsightsLayout = {
version: INSIGHTS_LAYOUT_VERSION,
// Every section ships default-visible; feature/data gates still apply
@@ -203,9 +178,11 @@ export const DEFAULT_INSIGHTS_LAYOUT: InsightsLayout = {
// `SUB_PAGE_SLUGS` order, which itself tracks the MeasurementCategory
// overlay: vitals → body → activity → sleep → cardiovascular → mood →
// events). The dense 0-based `order` mirrors the routed tab strip.
+ // Every tile defaults VISIBLE — see the module note above for why the
+ // v1.15.11 curated subset was a nav-pill regression.
tiles: INSIGHTS_TILE_IDS.map((id, order) => ({
id,
- visible: DEFAULT_VISIBLE_TILE_IDS.has(id),
+ visible: true,
order,
})),
};
diff --git a/src/lib/insights/__tests__/sub-page-manager-group.test.ts b/src/lib/insights/__tests__/sub-page-manager-group.test.ts
new file mode 100644
index 000000000..82c339234
--- /dev/null
+++ b/src/lib/insights/__tests__/sub-page-manager-group.test.ts
@@ -0,0 +1,49 @@
+import { describe, it, expect } from "vitest";
+
+import {
+ MANAGER_GROUP_ORDER,
+ SUB_PAGE_MANAGER_GROUP,
+ SUB_PAGE_MANAGER_GROUP_SLUGS,
+ SUB_PAGE_SLUGS,
+ SUB_PAGE_GROUP,
+} from "@/lib/insights/sub-page-metric";
+
+/**
+ * v1.15.14 W2 — the "Anpassen" sub-page manager lists EVERY routed
+ * sub-page slug grouped by category. These guards pin the total grouping
+ * so a future slug can never silently drop out of the manager.
+ */
+describe("sub-page manager grouping", () => {
+ it("assigns every slug to exactly one manager group", () => {
+ for (const slug of SUB_PAGE_SLUGS) {
+ const group = SUB_PAGE_MANAGER_GROUP[slug];
+ expect(group, `slug ${slug} has no manager group`).toBeDefined();
+ expect(MANAGER_GROUP_ORDER).toContain(group);
+ }
+ });
+
+ it("the grouped-slugs map partitions the full slug universe", () => {
+ const flattened = MANAGER_GROUP_ORDER.flatMap(
+ (group) => SUB_PAGE_MANAGER_GROUP_SLUGS[group],
+ );
+ // No slug missing.
+ expect(new Set(flattened)).toEqual(new Set(SUB_PAGE_SLUGS));
+ // No slug counted twice.
+ expect(flattened.length).toBe(SUB_PAGE_SLUGS.length);
+ });
+
+ it("is a superset of the tab-strip nav grouping (SUB_PAGE_GROUP agrees)", () => {
+ for (const slug of SUB_PAGE_SLUGS) {
+ const navGroup = SUB_PAGE_GROUP[slug];
+ if (navGroup) {
+ expect(SUB_PAGE_MANAGER_GROUP[slug]).toBe(navGroup);
+ }
+ }
+ });
+
+ it("covers the three categories the tab strip never collapses", () => {
+ expect(SUB_PAGE_MANAGER_GROUP_SLUGS.sleep.length).toBeGreaterThan(0);
+ expect(SUB_PAGE_MANAGER_GROUP_SLUGS.mood).toContain("mood");
+ expect(SUB_PAGE_MANAGER_GROUP_SLUGS.events).toContain("medications");
+ });
+});
diff --git a/src/lib/insights/sub-page-metric.ts b/src/lib/insights/sub-page-metric.ts
index 0659d6759..103034569 100644
--- a/src/lib/insights/sub-page-metric.ts
+++ b/src/lib/insights/sub-page-metric.ts
@@ -226,6 +226,95 @@ export const SUB_PAGE_GROUP_ORDER: Record
),
};
+/**
+ * v1.15.14 W2 — manager grouping for the "Anpassen" → "Detailseiten
+ * verwalten" surface.
+ *
+ * The tab strip only assigns a `SUB_PAGE_GROUP` to the long-tail metrics
+ * that collapse behind a popover parent pill; the eight legacy flat pills
+ * (blood-pressure, pulse, weight, bmi, sleep, mood, medications, workouts)
+ * render as direct entries and carry NO `SUB_PAGE_GROUP`. The sub-page
+ * MANAGER, by contrast, lists EVERY slug grouped under a category header,
+ * so it needs a total slug → group mapping that covers the flat pills too
+ * and adds the three categories the tab strip never groups (`sleep`,
+ * `mood`, `events`). This map is that total assignment; it is a superset
+ * of `SUB_PAGE_GROUP` (every `SUB_PAGE_GROUP` entry agrees with it) plus a
+ * fallback for the flat slugs. Single source of truth for the manager —
+ * the tab-strip nav grouping stays `SUB_PAGE_GROUP` exactly as before.
+ */
+export type ManagerGroup =
+ | "vitals"
+ | "body"
+ | "activity"
+ | "sleep"
+ | "cardiovascular"
+ | "hearing"
+ | "environment"
+ | "metabolic"
+ | "mood"
+ | "events";
+
+/**
+ * Render order of the manager's category sections — mirrors the
+ * MeasurementCategory overlay the rest of insights follows
+ * (vitals → body → activity → sleep → cardiovascular → hearing →
+ * environment → metabolic → mood → events).
+ */
+export const MANAGER_GROUP_ORDER: readonly ManagerGroup[] = [
+ "vitals",
+ "body",
+ "activity",
+ "sleep",
+ "cardiovascular",
+ "hearing",
+ "environment",
+ "metabolic",
+ "mood",
+ "events",
+];
+
+/** Explicit group for the flat tab-strip slugs (those `SUB_PAGE_GROUP` omits). */
+const FLAT_SLUG_MANAGER_GROUP: Partial> = {
+ "blood-pressure": "vitals",
+ pulse: "vitals",
+ weight: "body",
+ bmi: "body",
+ workouts: "activity",
+ sleep: "sleep",
+ mood: "mood",
+ medications: "events",
+};
+
+/**
+ * Total slug → manager-group assignment. A slug grouped by the tab strip
+ * keeps that group; a flat slug falls back to {@link FLAT_SLUG_MANAGER_GROUP}.
+ * Every slug resolves (asserted by the manager-coverage test).
+ */
+export const SUB_PAGE_MANAGER_GROUP: Record =
+ Object.fromEntries(
+ SUB_PAGE_SLUGS.map((slug) => [
+ slug,
+ (SUB_PAGE_GROUP[slug] as ManagerGroup | undefined) ??
+ FLAT_SLUG_MANAGER_GROUP[slug] ??
+ "vitals",
+ ]),
+ ) as unknown as Record;
+
+/**
+ * Slugs per manager group, in `SUB_PAGE_SLUGS` order. The manager renders
+ * one collapsible section per group following {@link MANAGER_GROUP_ORDER},
+ * each listing these slugs.
+ */
+export const SUB_PAGE_MANAGER_GROUP_SLUGS: Record<
+ ManagerGroup,
+ readonly SubPageSlug[]
+> = Object.fromEntries(
+ MANAGER_GROUP_ORDER.map((group) => [
+ group,
+ SUB_PAGE_SLUGS.filter((slug) => SUB_PAGE_MANAGER_GROUP[slug] === group),
+ ]),
+) as unknown as Record;
+
/**
* Mother-page route. Kept here as a named constant so call sites
* never have to hard-code the string.
|