feat(insights): stat strip, range band, all-readings subpage, diversity nudge

Add the data-value features to the insights category pages on top of the
density pass:

- Stat strip: lead each metric page with min / max / median / mean,
  read from the already-fetched analytics summaries slice (no new
  network). Median is now computed in both summary producers — exact in
  the live path, a windowed PERCENTILE_CONT in the rollup-tier slim
  slice. The thin HealthKit pages mount it too for parity; no AI
  assessment is added there.
- Range band: shade an Apple-Health-style min-max area around the daily
  mean line where the data supports it (non-cumulative metrics). The
  daily-aggregate read now emits per-day min / max from the rollup
  buckets; the band rides the existing target-range toggle.
- All readings: a dedicated per-metric subpage at /insights/values/<type>
  reusing the measurement list pinned to one type (pagination + inline
  edit/delete). A clear entry button mounts at the foot of each
  category page.
- Diversity nudge: a gentle hint when a metric's readings cluster on one
  weekday or time of day, computed from a bounded, cached timestamp
  window — non-blocking, self-gating.

All new reads stay cache-friendly and off the navigation path.
This commit is contained in:
Marc-André Bombeck committed 2026-06-01 20:26:26 +02:00
1 parent cf649f72ce
commit 6ce09cc76d
32 files changed
+1063 -37

No files matched your search

+11
View File
@@ -328,6 +328,8 @@ export const GET = apiHandler(async (request: NextRequest) => {
// legacy fallback below covers the boot-backfill convergence
// window when an existing bucket pre-dates v1.4.39.
sumValue: true,
minValue: true,
maxValue: true,
},
});
// v1.4.39.2 — coverage-mismatch fallback. v1.4.39.1 wired the
@@ -417,6 +419,8 @@ export const GET = apiHandler(async (request: NextRequest) => {
mean: true,
count: true,
sumValue: true,
minValue: true,
maxValue: true,
},
});
} catch (err) {
@@ -454,6 +458,13 @@ export const GET = apiHandler(async (request: NextRequest) => {
: r.mean,
measuredAt: r.bucketStart.toISOString(),
count: r.count,
// v1.8.5 — per-day min / max so the chart can shade an
// Apple-Health-style range band around the daily mean line.
// Omitted for cumulative metrics (steps, distance, …) where the
// daily value is a SUM, not a central tendency, so an intra-day
// spread band carries no meaning.
minValue: useSum ? undefined : r.minValue,
maxValue: useSum ? undefined : r.maxValue,
}));
annotate({
action: { name: "measurement.list" },