mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
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:
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",
|
||||
|
||||
@@ -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 => ({
|
||||
|
||||
Reference in new issue
Block a user