v1.4.36 — perf, charts, AI payload trim, UX punch

Heavy-aggregate skip on the rollup-fresh + fully-covered path,
daily chart fetches opt into rollup buckets via source=rollup,
Insights extractFeatures swap drops the prompt payload from
double-digit MB to the low hundreds of kB on power-user accounts,
page-level isLoading gate replaced with per-section early-skeleton
paint, trends-row chart slot pinned for CLS, IntakeHistoryListV2
restored on the medication detail page, About card folded into the
Admin Console with an auto-checking update badge, cumulative metric
tiles read the day's running sum, IP-whois fallback surfaces city
+ country when the ASN lookup misses. Migration 0068 adds an
additive users.insights_exclude_metrics column. Unit suite 4354
passing; typecheck + lint clean.
This commit is contained in:
Marc-André Bombeck committed 2026-05-17 13:22:51 +02:00
1 parent 5a3ca330ac
commit 0e2a99f8e7
70 files changed
+4982 -810

No files matched your search

@@ -266,8 +266,12 @@ describe("GET /api/analytics", () => {
// contract the dashboard tile strip reads.
it("returns the slim summaries slice when ?slice=summaries is set", async () => {
const fiveDaysAgo = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000);
// Aggregate pass + latest pass. The mock returns one row per pass.
// v1.4.36 — slim slice now opens with a cheap `SELECT COUNT(*) FROM
// measurement_rollups` probe. Force `n: 0` so the route takes the
// legacy heavy-aggregate fallback path that this test's fixtures
// expect (aggregate + latest, two `$queryRaw` passes).
vi.mocked(prisma.$queryRaw)
.mockResolvedValueOnce([{ n: BigInt(0) }] as never)
.mockResolvedValueOnce([
{
type: "WEIGHT",
+61 -14
View File
@@ -29,10 +29,38 @@ import type {
} from "@/generated/prisma/client";
import { measurementTypeEnum } from "@/lib/validations/measurement";
import { pickCanonicalSourceRows } from "@/lib/analytics/source-priority";
import type { SourcePriorityMetricKey } from "@/lib/validations/source-priority";
import { ensureUserRollupsFresh } from "@/lib/measurements/rollups";
import {
isCumulativeDaySumType,
pickCumulativeDaySum,
} from "@/lib/measurements/cumulative-day-sum";
export const dynamic = "force-dynamic";
/**
* v1.4.36 W4c — cumulative MeasurementType → SourcePriorityMetricKey
* for `pickCanonicalSourceRows`. Returns `null` for types without a
* dedicated priority ladder (e.g. TIME_IN_DAYLIGHT), which fall
* through the picker's "no ladder" pass-through branch.
*/
function cumulativeMetricKey(
type: MeasurementType,
): SourcePriorityMetricKey | null {
switch (type) {
case "ACTIVITY_STEPS":
return "steps";
case "ACTIVE_ENERGY_BURNED":
return "activeEnergy";
case "WALKING_RUNNING_DISTANCE":
return "walkingRunningDistance";
case "FLIGHTS_CLIMBED":
return "flightsClimbed";
default:
return null;
}
}
/**
* v1.4.33 C1 — pull `?slice=…` from either a NextRequest (the
* route's production wrapper) or a plain `Request` (the integration
@@ -208,21 +236,40 @@ async function buildAnalyticsResponse(user: AuthedUser) {
sourcePriorityJson,
(d) => userDayKey(d, userTz),
).canonicalRows;
const byDay = new Map<string, { total: number; date: Date }>();
for (const m of sleepRows) {
const key = userDayKey(m.measuredAt, userTz);
const slot = byDay.get(key) ?? {
total: 0,
date: m.measuredAt,
};
slot.total += m.value;
if (m.measuredAt > slot.date) slot.date = m.measuredAt;
byDay.set(key, slot);
}
datapoints = Array.from(byDay.values()).map(
(s): DataPoint => ({ date: s.date, value: s.total }),
datapoints = pickCumulativeDaySum(sleepRows, (d) =>
userDayKey(d, userTz),
);
} else if (isCumulativeDaySumType(type)) {
// v1.4.36 W4c — broaden the SLEEP_DURATION day-bucket-sum
// pattern to every cumulative metric (steps, active energy,
// walking + running distance, flights climbed, time in
// daylight). Apple Health writes minute-level slices for
// these series; the dashboard tile was reading the
// *latest slice* via `summary.latest` instead of the day's
// total, which Marc reported as "Steps tile shows
// last-measurement-not-day-sum". After the per-day sum
// collapse, `summary.latest` is the most-recent day's
// total — exactly what the tile expects.
//
// Map cumulative MeasurementType → SourcePriorityMetricKey
// for the canonical-row picker. TIME_IN_DAYLIGHT has no
// dedicated priority ladder (no clinical-grade competitor
// to Apple Health for daylight minutes today) so it falls
// through `pickCanonicalSourceRows`'s "no ladder"
// pass-through branch — we ALWAYS bucket-and-sum it
// regardless of source.
const metricKey = cumulativeMetricKey(type);
const canonicalRows = metricKey
? pickCanonicalSourceRows(
measurements,
metricKey,
sourcePriorityJson,
(d) => userDayKey(d, userTz),
).canonicalRows
: measurements;
datapoints = pickCumulativeDaySum(canonicalRows, (d) =>
userDayKey(d, userTz),
);
datapoints.sort((a, b) => a.date.getTime() - b.date.getTime());
} else {
datapoints = measurements.map(
(m): DataPoint => ({