mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
feat(dashboard): fluid-intake strip tile, nutrients-store-backed
Reuses the TrendCard strip-tile pattern (icon, neutral direction sentiment, stale-days caption) for a new waterIntake widget, but sources it from NutrientIntakeDay instead of introducing a parallel MeasurementType. The dashboard snapshot builder reads the last 30 day-keys for nutrient="water" (summed across sources), folds them into a synthetic NUTRIENT_WATER summary key alongside the real MeasurementType summaries, and the existing module-gating machinery (SUMMARY_TYPE_MODULE, WIDGET_MODULE_BY_ID) strips it when the opt-in nutrients module is off — no new gating code path, just two new map entries. The tile links out to /insights/nutrients instead of a measurement detail page. Widget catalogue grows 37 -> 38 ids.
This commit is contained in:
1 parent
da2de015fe
commit
b396b51d35
9 files changed
+200
-33
No files matched your search
@@ -111,8 +111,9 @@ describe("resolveConfiguredTileCount (v1.16.8)", () => {
|
||||
// silhouette. bp paints sys + dia = 2. v1.28.52 added seven vitals +
|
||||
// body-composition strip tiles (hrv, oxygenSaturation, respiratoryRate,
|
||||
// wristTemperature, muscleMass, totalBodyWater, boneMass), each
|
||||
// single-count: 11 + 7 = 18.
|
||||
expect(resolveConfiguredTileCount(DEFAULT_DASHBOARD_LAYOUT)).toBe(18);
|
||||
// single-count: 11 + 7 = 18. v1.29 added the fluid-intake strip tile
|
||||
// (waterIntake), single-count: 18 + 1 = 19.
|
||||
expect(resolveConfiguredTileCount(DEFAULT_DASHBOARD_LAYOUT)).toBe(19);
|
||||
});
|
||||
|
||||
it("ignores chart-only and iOS-pin-only widget ids", () => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Dumbbell,
|
||||
Footprints,
|
||||
Gauge,
|
||||
GlassWater,
|
||||
Heart,
|
||||
HeartPulse,
|
||||
Moon,
|
||||
@@ -173,6 +174,9 @@ const TILE_INSIGHT_HREF: Record<string, string> = {
|
||||
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 {
|
||||
@@ -394,6 +398,10 @@ export default function DashboardPageClient() {
|
||||
const muscleMassSummary = data?.summaries?.MUSCLE_MASS;
|
||||
const bodyWaterSummary = data?.summaries?.TOTAL_BODY_WATER;
|
||||
const boneMassSummary = data?.summaries?.BONE_MASS;
|
||||
// v1.29 — fluid intake strip tile. Synthetic summary key, not a real
|
||||
// MeasurementType — derived server-side from `NutrientIntakeDay`
|
||||
// (nutrient="water", summed across sources); see `dashboard/snapshot.ts`.
|
||||
const waterIntakeSummary = data?.summaries?.NUTRIENT_WATER;
|
||||
const moodSummary = moodData?.summary;
|
||||
|
||||
// Resolve full dashboard layout — controls visibility + order of every widget
|
||||
@@ -478,6 +486,8 @@ export default function DashboardPageClient() {
|
||||
const hasMuscleMass = (muscleMassSummary?.count ?? 0) > 0;
|
||||
const hasBodyWater = (bodyWaterSummary?.count ?? 0) > 0;
|
||||
const hasBoneMass = (boneMassSummary?.count ?? 0) > 0;
|
||||
// v1.29 — data floor for the fluid-intake strip tile.
|
||||
const hasWaterIntake = (waterIntakeSummary?.count ?? 0) > 0;
|
||||
/**
|
||||
* v1.4.33 F4 — gate the BD-Zielbereich tile so a literal "0,0 %"
|
||||
* placeholder doesn't ride on the dashboard when none of the user's
|
||||
@@ -526,6 +536,9 @@ export default function DashboardPageClient() {
|
||||
const showMuscleMassTile = isTileVisible("muscleMass") && hasMuscleMass;
|
||||
const showBodyWaterTile = isTileVisible("totalBodyWater") && hasBodyWater;
|
||||
const showBoneMassTile = isTileVisible("boneMass") && hasBoneMass;
|
||||
// v1.29 — fluid intake strip tile gate: layout toggle AND a non-empty
|
||||
// summary, same precedent as the vitals/body-composition tiles above.
|
||||
const showWaterIntakeTile = isTileVisible("waterIntake") && hasWaterIntake;
|
||||
const showBpInTargetTile = isTileVisible("bpInTarget") && hasBpInTarget;
|
||||
|
||||
// Chart (lower row) gates — controlled by the legacy `visible` flag.
|
||||
@@ -1280,6 +1293,32 @@ export default function DashboardPageClient() {
|
||||
),
|
||||
});
|
||||
}
|
||||
// v1.29 — fluid intake strip tile. Neutral sentiment: more water is
|
||||
// not unambiguously "better" (over-hydration is a real risk), so no
|
||||
// up/down framing — same posture as wrist temperature / body water.
|
||||
if (showWaterIntakeTile) {
|
||||
trendCards.push({
|
||||
id: "waterIntake",
|
||||
order: widgetOrder("waterIntake"),
|
||||
node: (
|
||||
<TrendCard
|
||||
key="waterIntake"
|
||||
label={t("nutrients.names.water")}
|
||||
latest={waterIntakeSummary?.latest ?? null}
|
||||
unit="mL"
|
||||
avg7={waterIntakeSummary?.avg7 ?? null}
|
||||
avg30={waterIntakeSummary?.avg30 ?? null}
|
||||
slope30={waterIntakeSummary?.slope30 ?? null}
|
||||
trend7Delta={summaryToTrend7Delta(waterIntakeSummary)}
|
||||
icon={GlassWater}
|
||||
directionSentiment="neutral"
|
||||
compareBaseline={compareBaseline}
|
||||
compareDelta={tileCompareDelta(waterIntakeSummary)}
|
||||
staleDays={tileStaleDays("NUTRIENT_WATER")}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
if (showBpInTargetTile) {
|
||||
/* v1.4.22 A2 — feature parity with every other tile.
|
||||
Synthesise a slope from the difference between the 7-day
|
||||
|
||||
@@ -64,6 +64,9 @@ const TILE_CAPABLE_WIDGET_IDS = new Set<string>([
|
||||
"muscleMass",
|
||||
"totalBodyWater",
|
||||
"boneMass",
|
||||
// v1.29 — fluid intake strip tile: paints one strip card and self-gates
|
||||
// on having a NUTRIENT_WATER sample.
|
||||
"waterIntake",
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -162,6 +162,8 @@ const WIDGET_LABEL_KEYS: Record<DashboardWidgetId, string> = {
|
||||
walkingSteadiness: "measurements.typeWalkingSteadiness",
|
||||
// v1.18.2 — Vorsorge preventive-care summary card.
|
||||
vorsorge: "measurementReminders.sectionTitle",
|
||||
// v1.29 — fluid intake strip tile (nutrients-store-backed).
|
||||
waterIntake: "nutrients.names.water",
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -447,10 +447,13 @@ describe("DASHBOARD_WIDGET_CATALOGUE_IDS — 27-id catalogue", () => {
|
||||
// web-writable (iOS-only 11 → 9), and muscle mass is a brand-new
|
||||
// writable id, so the server-known set grew 25 → 28 and the catalogue
|
||||
// 36 → 37.
|
||||
expect(DASHBOARD_WIDGET_IDS).toHaveLength(28);
|
||||
// v1.29 — the nutrients-store-backed fluid-intake strip tile
|
||||
// (`waterIntake`) added one server-known id, so the catalogue grew
|
||||
// 37 → 38.
|
||||
expect(DASHBOARD_WIDGET_IDS).toHaveLength(29);
|
||||
expect(DASHBOARD_IOS_ONLY_WIDGET_IDS).toHaveLength(9);
|
||||
expect(DASHBOARD_WIDGET_CATALOGUE_IDS).toHaveLength(37);
|
||||
expect(new Set(DASHBOARD_WIDGET_CATALOGUE_IDS).size).toBe(37);
|
||||
expect(DASHBOARD_WIDGET_CATALOGUE_IDS).toHaveLength(38);
|
||||
expect(new Set(DASHBOARD_WIDGET_CATALOGUE_IDS).size).toBe(38);
|
||||
});
|
||||
|
||||
it("ships the sleep / steps / glucose strip tiles default-on (v1.20.0)", () => {
|
||||
@@ -677,17 +680,17 @@ describe("resolveDashboardLayout() — iOS-only id retention (v1.7.0)", () => {
|
||||
expect(ids).not.toContain("totally-made-up");
|
||||
});
|
||||
|
||||
it("keeps the default layout at the 28 web tiles (no iOS-only seeded)", () => {
|
||||
it("keeps the default layout at the 29 web tiles (no iOS-only seeded)", () => {
|
||||
const ids = DEFAULT_DASHBOARD_LAYOUT.widgets.map((w) => w.id);
|
||||
expect(ids).toHaveLength(28);
|
||||
expect(ids).toHaveLength(29);
|
||||
for (const iosId of DASHBOARD_IOS_ONLY_WIDGET_IDS) {
|
||||
expect(ids).not.toContain(iosId);
|
||||
}
|
||||
});
|
||||
|
||||
it("does NOT auto-append iOS-only ids when a web-only layout is read", () => {
|
||||
// A web account that saved only `weight` must auto-upgrade to the 28
|
||||
// web defaults — never to the 37 catalogue. iOS-only ids appear only
|
||||
// A web account that saved only `weight` must auto-upgrade to the 29
|
||||
// web defaults — never to the 38 catalogue. iOS-only ids appear only
|
||||
// once a native client has explicitly sent them.
|
||||
const partial = {
|
||||
version: 1,
|
||||
@@ -695,7 +698,7 @@ describe("resolveDashboardLayout() — iOS-only id retention (v1.7.0)", () => {
|
||||
};
|
||||
const resolved = resolveDashboardLayout(partial);
|
||||
const ids = resolved.widgets.map((w) => w.id);
|
||||
expect(ids).toHaveLength(28);
|
||||
expect(ids).toHaveLength(29);
|
||||
for (const iosId of DASHBOARD_IOS_ONLY_WIDGET_IDS) {
|
||||
expect(ids).not.toContain(iosId);
|
||||
}
|
||||
|
||||
@@ -72,6 +72,10 @@ export const DASHBOARD_WIDGET_IDS = [
|
||||
// user opts in via Settings → Dashboard. Always-on data surface (no
|
||||
// toggleable module gate), so it behaves as a core opt-in tile.
|
||||
"vorsorge",
|
||||
// v1.29 — fluid intake (drinking water). Nutrients-store-backed (see
|
||||
// `NUTRIENT_WATER` in the dashboard snapshot), gated on the `nutrients`
|
||||
// module; self-gates on data like the v1.28.52 strip tiles.
|
||||
"waterIntake",
|
||||
] as const;
|
||||
|
||||
export type DashboardWidgetId = (typeof DASHBOARD_WIDGET_IDS)[number];
|
||||
@@ -611,6 +615,12 @@ export const DEFAULT_DASHBOARD_LAYOUT: DashboardLayout = {
|
||||
// preventive-care reminders surface on / by default; the card self-gates
|
||||
// (see page.tsx) and shows nothing for accounts without due reminders.
|
||||
{ id: "vorsorge", visible: true, tileVisible: false, order: 27 },
|
||||
// v1.29 — fluid intake strip tile. On by default (`tileVisible: true`)
|
||||
// like the v1.28.52 vitals cluster; self-gates on having water data
|
||||
// (see `showWaterIntakeTile` in page-client.tsx) AND on the `nutrients`
|
||||
// module (opt-in, default off), so an account that never enabled
|
||||
// nutrients or never logged water sees a clean dashboard.
|
||||
{ id: "waterIntake", visible: false, tileVisible: true, order: 28 },
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -124,6 +124,8 @@ const emptySummary = {
|
||||
const fakePrisma = {
|
||||
measurement: { findMany: vi.fn().mockResolvedValue([]) },
|
||||
moodEntry: { findMany: vi.fn().mockResolvedValue([]) },
|
||||
// v1.29 — the fluid-intake dashboard tile block reads this table.
|
||||
nutrientIntakeDay: { findMany: vi.fn().mockResolvedValue([]) },
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any;
|
||||
|
||||
@@ -168,6 +170,7 @@ beforeEach(() => {
|
||||
});
|
||||
fakePrisma.measurement.findMany.mockResolvedValue([]);
|
||||
fakePrisma.moodEntry.findMany.mockResolvedValue([]);
|
||||
fakePrisma.nutrientIntakeDay.findMany.mockResolvedValue([]);
|
||||
hasAnyConfiguredProvider.mockResolvedValue(true);
|
||||
buildMedsTodayBlock.mockResolvedValue({
|
||||
activeCount: 1,
|
||||
@@ -749,11 +752,11 @@ describe("buildDashboardSnapshot — layoutCatalogue (35-id round-trip)", () =>
|
||||
isFullyCovered.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("emits all 37 catalogue ids with visibility + order", async () => {
|
||||
it("emits all 38 catalogue ids with visibility + order", async () => {
|
||||
const snap = await buildDashboardSnapshot(fakePrisma, baseUser());
|
||||
expect(snap.layoutCatalogue).toHaveLength(37);
|
||||
expect(snap.layoutCatalogue).toHaveLength(38);
|
||||
const ids = new Set(snap.layoutCatalogue.map((w) => w.id));
|
||||
expect(ids.size).toBe(37);
|
||||
expect(ids.size).toBe(38);
|
||||
// iOS-only ids appended default-invisible.
|
||||
const walkingSpeed = snap.layoutCatalogue.find(
|
||||
(w) => w.id === "walkingSpeed",
|
||||
@@ -826,7 +829,7 @@ describe("buildDashboardSnapshot — additive proof", () => {
|
||||
expect(snap.layout).toHaveProperty("version");
|
||||
expect(Array.isArray(snap.layout.widgets)).toBe(true);
|
||||
// Resolved layout still carries only server-known widget ids.
|
||||
expect(snap.layout.widgets.length).toBeLessThanOrEqual(28);
|
||||
expect(snap.layout.widgets.length).toBeLessThanOrEqual(29);
|
||||
|
||||
// tiles shape unchanged.
|
||||
expect(snap.tiles).toHaveProperty("summaries");
|
||||
@@ -852,6 +855,8 @@ describe("module gating maps — every toggleable-domain widget is mapped (v1.18
|
||||
"stairAscentSpeed",
|
||||
"stairDescentSpeed",
|
||||
"breathingDisturbances",
|
||||
// v1.29 — fluid-intake strip tile, nutrients-store-backed.
|
||||
"waterIntake",
|
||||
] as const;
|
||||
|
||||
it("every toggleable-domain widget id has a module mapping", () => {
|
||||
@@ -896,6 +901,11 @@ describe("module gating maps — every toggleable-domain widget is mapped (v1.18
|
||||
expect(WIDGET_MODULE_BY_ID.breathingDisturbances).toBe("sleep");
|
||||
expect(SUMMARY_TYPE_MODULE.BREATHING_DISTURBANCES).toBe("sleep");
|
||||
});
|
||||
|
||||
it("fluid-intake widget + synthetic summary type resolve to the nutrients module (v1.29)", () => {
|
||||
expect(WIDGET_MODULE_BY_ID.waterIntake).toBe("nutrients");
|
||||
expect(SUMMARY_TYPE_MODULE.NUTRIENT_WATER).toBe("nutrients");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildDashboardSnapshot — module gating (v1.18.0)", () => {
|
||||
@@ -953,6 +963,11 @@ describe("buildDashboardSnapshot — module gating (v1.18.0)", () => {
|
||||
fakePrisma.moodEntry.findMany.mockResolvedValue([
|
||||
{ date: new Date(), score: 4 },
|
||||
]);
|
||||
// v1.29 — water rows so the fluid-intake tile would otherwise carry a
|
||||
// summary (proving the nutrients module actually strips it below).
|
||||
fakePrisma.nutrientIntakeDay.findMany.mockResolvedValue([
|
||||
{ day: new Date().toISOString().slice(0, 10), amount: 1500 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("all modules on: every toggleable tile + its data is present", async () => {
|
||||
@@ -1025,6 +1040,25 @@ describe("buildDashboardSnapshot — module gating (v1.18.0)", () => {
|
||||
expect(widget(snap, "achievements")!.visible).toBe(false);
|
||||
});
|
||||
|
||||
it("nutrients on: fluid-intake tile widget carries the synthetic NUTRIENT_WATER summary (v1.29)", async () => {
|
||||
const snap = await buildDashboardSnapshot(fakePrisma, baseUser(), {
|
||||
modules: () => Promise.resolve(moduleMap()),
|
||||
});
|
||||
expect(snap.tiles.summaries.NUTRIENT_WATER).toBeDefined();
|
||||
expect(snap.tiles.summaries.NUTRIENT_WATER!.latest).toBe(1500);
|
||||
});
|
||||
|
||||
it("nutrients disabled: waterIntake tile hidden + NUTRIENT_WATER stripped from summaries (v1.29)", async () => {
|
||||
const snap = await buildDashboardSnapshot(fakePrisma, baseUser(), {
|
||||
modules: () => Promise.resolve(moduleMap({ nutrients: false })),
|
||||
});
|
||||
expect(widget(snap, "waterIntake")!.visible).toBe(false);
|
||||
expect(widget(snap, "waterIntake")!.tileVisible).toBe(false);
|
||||
expect(cat(snap, "waterIntake")!.visible).toBe(false);
|
||||
expect(snap.tiles.summaries.NUTRIENT_WATER).toBeUndefined();
|
||||
expect(snap.tiles.lastSeenByType.NUTRIENT_WATER).toBeUndefined();
|
||||
});
|
||||
|
||||
it("insights disabled: Daily Briefing surface goes to disabled state", async () => {
|
||||
const snap = await buildDashboardSnapshot(
|
||||
fakePrisma,
|
||||
|
||||
@@ -514,6 +514,50 @@ async function buildMoodBlock(
|
||||
return { summary: series.summary, entries: series.entries };
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.29 — fluid-intake dashboard tile block, read from `NutrientIntakeDay`
|
||||
* (nutrient="water", last 30 day-keys, summed ACROSS SOURCES per day —
|
||||
* migration 0249 can carry an APPLE_HEALTH row and a MANUAL row for the
|
||||
* same day). ≤ 60 rows off the existing `(userId, nutrient, day desc)`
|
||||
* index. Sparse by construction (only days with an actual write become a
|
||||
* `DataPoint`) — matching the convention every real `MeasurementType`
|
||||
* summary follows: an unlogged day is absent, not a zero reading. `null`
|
||||
* when there is no water data in the window; the caller strips the
|
||||
* synthetic `NUTRIENT_WATER` key entirely in that case (mirrors mood).
|
||||
* Always fetched (cheap, indexed) and stripped post-hoc by
|
||||
* `gateSummariesByModules` when the `nutrients` module is off — the same
|
||||
* fetch-then-gate posture the sleep/glucose/recovery types already use.
|
||||
*/
|
||||
async function buildNutrientWaterBlock(
|
||||
prisma: PrismaClient,
|
||||
userId: string,
|
||||
): Promise<{ summary: DataSummary; lastSeenAt: string } | null> {
|
||||
const since = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
|
||||
.toISOString()
|
||||
.slice(0, 10);
|
||||
const rows = await prisma.nutrientIntakeDay.findMany({
|
||||
where: { userId, nutrient: "water", day: { gte: since } },
|
||||
select: { day: true, amount: true },
|
||||
});
|
||||
if (rows.length === 0) return null;
|
||||
|
||||
const sumByDay = new Map<string, number>();
|
||||
for (const row of rows) {
|
||||
sumByDay.set(row.day, (sumByDay.get(row.day) ?? 0) + row.amount);
|
||||
}
|
||||
const points: DataPoint[] = [...sumByDay.entries()]
|
||||
.map(([day, amount]) => ({
|
||||
date: new Date(`${day}T00:00:00.000Z`),
|
||||
value: amount,
|
||||
}))
|
||||
.sort((a, b) => a.date.getTime() - b.date.getTime());
|
||||
|
||||
return {
|
||||
summary: summarize(points),
|
||||
lastSeenAt: points[points.length - 1].date.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Thick slice — BD-Zielbereich + per-context glucose + health score.
|
||||
* Only called by the builder when the rollup tier is warm for the
|
||||
@@ -1104,6 +1148,7 @@ export async function buildDashboardSnapshot(
|
||||
medsToday,
|
||||
modules,
|
||||
scoreRings,
|
||||
nutrientWater,
|
||||
] = await Promise.all([
|
||||
// A5 — reuse the coverage map already probed above so the slice
|
||||
// doesn't re-run the identical `probeRollupCoverage` query.
|
||||
@@ -1131,8 +1176,27 @@ export async function buildDashboardSnapshot(
|
||||
await medsTodayPromise,
|
||||
),
|
||||
).catch(() => [] as DashboardScoreRing[]),
|
||||
// v1.29 — fluid-intake tile block (see `buildNutrientWaterBlock`).
|
||||
time("nutrientWater", () => buildNutrientWaterBlock(prisma, user.id)),
|
||||
]);
|
||||
|
||||
// v1.29 — fold the water block into the slim slice under a synthetic
|
||||
// `NUTRIENT_WATER` key BEFORE the module gate below strips it (like
|
||||
// every other summary type) when `nutrients` is off. Not a real
|
||||
// `MeasurementType` — see the widened `SUMMARY_TYPE_MODULE` note.
|
||||
if (nutrientWater) {
|
||||
const lastMs = new Date(nutrientWater.lastSeenAt).getTime();
|
||||
const daysAgo = Math.floor((nowMs - lastMs) / (24 * 60 * 60 * 1000));
|
||||
slimRaw.summaries = {
|
||||
...slimRaw.summaries,
|
||||
NUTRIENT_WATER: nutrientWater.summary,
|
||||
};
|
||||
slimRaw.lastSeenByType = {
|
||||
...slimRaw.lastSeenByType,
|
||||
NUTRIENT_WATER: { lastSeenAt: nutrientWater.lastSeenAt, daysAgo },
|
||||
};
|
||||
}
|
||||
|
||||
// v1.18.0 — strip disabled-module data before it leaves the server.
|
||||
// Mood is blanked when the mood module is off; sleep / glucose summary
|
||||
// types are dropped from the slim slice; the glucose clinical panel +
|
||||
|
||||
@@ -10,10 +10,8 @@
|
||||
* both for the server call sites and the existing tests.
|
||||
*
|
||||
* `ModuleKey` comes from `@/lib/modules/registry` (pure constants, no
|
||||
* imports) and `MeasurementType` is a type-only import from the generated
|
||||
* Prisma client, so this file stays browser-bundle-safe.
|
||||
* imports), so this file stays browser-bundle-safe.
|
||||
*/
|
||||
import type { MeasurementType } from "@/generated/prisma/client";
|
||||
import type { ModuleKey } from "@/lib/modules/registry";
|
||||
|
||||
/**
|
||||
@@ -41,26 +39,39 @@ export const WIDGET_MODULE_BY_ID: Partial<Record<string, ModuleKey>> = {
|
||||
stairAscentSpeed: "recovery",
|
||||
stairDescentSpeed: "recovery",
|
||||
breathingDisturbances: "sleep",
|
||||
// v1.29 — fluid-intake strip tile, nutrients-store-backed (see
|
||||
// `SUMMARY_TYPE_MODULE.NUTRIENT_WATER` below).
|
||||
waterIntake: "nutrients",
|
||||
};
|
||||
|
||||
/**
|
||||
* `MeasurementType` slim-summary keys that belong to a toggleable module.
|
||||
* When the module is off the key is stripped from `tiles.summaries` /
|
||||
* Slim-summary keys that belong to a toggleable module. When the module
|
||||
* is off the key is stripped from `tiles.summaries` /
|
||||
* `tiles.lastSeenByType` (so `metricStates` and the client data-floor
|
||||
* gates also drop it) before the snapshot leaves the server. Core vital
|
||||
* types are absent here and always pass through.
|
||||
*
|
||||
* v1.29 — widened from `Partial<Record<MeasurementType, ModuleKey>>` to
|
||||
* `Partial<Record<string, ModuleKey>>` so the synthetic `NUTRIENT_WATER`
|
||||
* key (a `NutrientIntakeDay`-derived summary, not a real
|
||||
* `MeasurementType` — the abandoned `feat/water` branch's parallel
|
||||
* `WATER_INTAKE` enum value is deliberately NOT added) can ride the same
|
||||
* gate without widening the `MeasurementType` enum itself.
|
||||
*/
|
||||
export const SUMMARY_TYPE_MODULE: Partial<Record<MeasurementType, ModuleKey>> =
|
||||
{
|
||||
SLEEP_DURATION: "sleep",
|
||||
BLOOD_GLUCOSE: "glucose",
|
||||
// v1.18.0 B1 — recovery-domain HealthKit metrics. The recovery page +
|
||||
// its tiles are the recovery module's surface; when it is off these
|
||||
// device-native signals must drop from the dashboard snapshot too.
|
||||
CARDIO_RECOVERY: "recovery",
|
||||
SIX_MINUTE_WALK_DISTANCE: "recovery",
|
||||
STAIR_ASCENT_SPEED: "recovery",
|
||||
STAIR_DESCENT_SPEED: "recovery",
|
||||
// Per-night breathing-disturbance index is a sleep-page signal.
|
||||
BREATHING_DISTURBANCES: "sleep",
|
||||
};
|
||||
export const SUMMARY_TYPE_MODULE: Partial<Record<string, ModuleKey>> = {
|
||||
SLEEP_DURATION: "sleep",
|
||||
BLOOD_GLUCOSE: "glucose",
|
||||
// v1.18.0 B1 — recovery-domain HealthKit metrics. The recovery page +
|
||||
// its tiles are the recovery module's surface; when it is off these
|
||||
// device-native signals must drop from the dashboard snapshot too.
|
||||
CARDIO_RECOVERY: "recovery",
|
||||
SIX_MINUTE_WALK_DISTANCE: "recovery",
|
||||
STAIR_ASCENT_SPEED: "recovery",
|
||||
STAIR_DESCENT_SPEED: "recovery",
|
||||
// Per-night breathing-disturbance index is a sleep-page signal.
|
||||
BREATHING_DISTURBANCES: "sleep",
|
||||
// v1.29 — fluid-intake dashboard tile summary, derived server-side from
|
||||
// `NutrientIntakeDay` (nutrient="water", summed across sources). Gated
|
||||
// on the `nutrients` module like the rest of that store's surfaces.
|
||||
NUTRIENT_WATER: "nutrients",
|
||||
};
|
||||
Reference in new issue
Block a user