mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
perf(charts): bound health-chart fetches to the active range window
The chart-data query in health-chart.tsx walked the full measurement history with a `while (true)` paginated loop on every visit. For Apple-Health-rich accounts (pulse every minute, months of backfill) this meant tens of thousands of rows per type per page navigation, which is the root cause behind FB-D2's "pulse insights loads forever" report. Derive `from` / `to` from the active range tab (plus the comparison-baseline shift when overlay is on), pass both to /api/measurements, and drop the unbounded paginator. The query key re-keys on the window so a range-tab change refetches the right slice. staleTime 60s + gcTime 5min hold the bounded payload across tab navigation between insights sub-pages. New `aggregate` query param + range-aggregation helper let the GET handler downsample server-side for ranges over 90 d (one row per day) and over 365 d (one row per ISO week). The per-request `limit` ceiling lifts from 500 to 5000 — still bounded, but enough headroom for an explicit from/to window without re-introducing pagination. iOS contract — GET /api/measurements: additive. The `from`, `to`, `aggregate` params are all optional; callers that omit them (the iOS client, the existing measurements page) see the unchanged response shape and the historical 500-row default cap. The aggregated branch is only entered when both from + to are present AND the window is wide enough for the threshold ladder to engage.
This commit is contained in:
1 parent
538b44f74d
commit
b00be2863d
6 files changed
+471
-47
No files matched your search
@@ -14,6 +14,11 @@ import {
|
||||
listMeasurementsSchema,
|
||||
getUnitForType,
|
||||
} from "@/lib/validations/measurement";
|
||||
import {
|
||||
aggregateRows,
|
||||
pickAggregateGrain,
|
||||
rangeLengthDays,
|
||||
} from "@/lib/measurements/range-aggregation";
|
||||
import { withIdempotency } from "@/lib/idempotency";
|
||||
import { NextRequest } from "next/server";
|
||||
import type {
|
||||
@@ -32,7 +37,8 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
return apiError(parsed.error.issues[0].message, 422);
|
||||
}
|
||||
|
||||
const { type, from, to, limit, offset, sortBy, sortDir } = parsed.data;
|
||||
const { type, from, to, limit, offset, sortBy, sortDir, aggregate } =
|
||||
parsed.data;
|
||||
|
||||
const where = {
|
||||
userId: user.id,
|
||||
@@ -47,6 +53,47 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
: {}),
|
||||
};
|
||||
|
||||
// v1.4.28 FB-D2 — when the caller supplies a from/to window AND asks
|
||||
// for an aggregated grain (or the window is wide enough that the
|
||||
// default grain should downsample), collapse to one row per bucket
|
||||
// per type before returning. Raw mode keeps the historical wire shape
|
||||
// so existing callers and the iOS client (which does not pass
|
||||
// `aggregate`) see no change.
|
||||
if (from && to) {
|
||||
const grain = pickAggregateGrain(rangeLengthDays(from, to), aggregate);
|
||||
if (grain !== "raw") {
|
||||
const rows = await prisma.measurement.findMany({
|
||||
where,
|
||||
orderBy: { measuredAt: "asc" },
|
||||
take: limit,
|
||||
skip: offset,
|
||||
select: { type: true, value: true, measuredAt: true },
|
||||
});
|
||||
const buckets = aggregateRows(
|
||||
rows.map((r) => ({
|
||||
type: r.type as string,
|
||||
value: r.value,
|
||||
measuredAt: r.measuredAt,
|
||||
})),
|
||||
grain,
|
||||
);
|
||||
const measurements = buckets.map((b) => ({
|
||||
type: b.type,
|
||||
value: b.avg,
|
||||
measuredAt: b.bucketStart.toISOString(),
|
||||
count: b.count,
|
||||
}));
|
||||
annotate({
|
||||
action: { name: "measurement.list" },
|
||||
meta: { total: measurements.length, type, aggregate: grain },
|
||||
});
|
||||
return apiSuccess({
|
||||
measurements,
|
||||
meta: { total: measurements.length, limit, offset, aggregate: grain },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const [measurements, total] = await Promise.all([
|
||||
prisma.measurement.findMany({
|
||||
where,
|
||||
|
||||
Reference in new issue
Block a user