mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
chore(release): v1.4.48 — backlog closure, perf cap, Coach + Withings + onboarding polish
Ships the deferred Mediums and Lows from the v1.4.47 audit round. Greens up the workout-batch integration suite that had been red on main for three releases. Extends the v1.4.47.1 90-day outer-WHERE cap to the cold-mount /api/analytics fallback so the cold slim slice drops from multi-second to sub-second on tenants with large measurement partitions. Adds the admin notifications-diagnostic endpoint that the v1.4.47 APNs investigation needed but did not have. Same commit closes H1 — the v1.4.45 and v1.4.46 CHANGELOG sections are scrubbed of internal release-process taxonomy. Code comments in summaries-slice no longer reference specific tenant row counts; the attempt-number runbook references migration 0077 instead of the stale 0076. Deferred: - Server-side suppression of MEDICATION_REMINDER APNs for iOS v0.6.0.7+ clients (issue #206) — waits for iOS v0.6.0.8 to ship the notification-permission re-trigger fix (healthlog-iOS#10). - /api/admin/notifications/diagnostic OpenAPI entry — admin-only, internal consumer today; v1.4.49 lands the iOS-side companion.
This commit is contained in:
1 parent
c8b6eb761b
commit
36bf778fd8
4 files changed
+130
-75
No files matched your search
@@ -71,7 +71,7 @@ import { readBestGranularityRollups } from "@/lib/rollups/measurement-read-wmy";
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* v1.4.48 M0 — all-time aggregate row used on the cold-mount fallback
|
||||
* v1.4.48 — all-time aggregate row used on the cold-mount fallback
|
||||
* path. Holds the linearly composable columns (`count / min / max /
|
||||
* mean`) that must reflect every row the user has ever logged for
|
||||
* that type, so this query intentionally has no `measured_at` cap.
|
||||
@@ -93,8 +93,9 @@ interface AllTimeAggregateRow {
|
||||
/**
|
||||
* v1.4.37.2 — shape returned by the per-type GROUP BY over
|
||||
* `measurement_rollups`. Replaces the v1.4.35 row-per-bucket transfer
|
||||
* that materialised ~306k rows on a power-user account; the SQL now
|
||||
* does the aggregation server-side and hands back one row per type.
|
||||
* that materialised a six-figure row count on tenants with large
|
||||
* measurement partitions; the SQL now does the aggregation
|
||||
* server-side and hands back one row per type.
|
||||
*/
|
||||
interface RollupAggregateRow {
|
||||
type: string;
|
||||
@@ -194,8 +195,8 @@ export async function computeSummariesSlice(
|
||||
): Promise<SummariesSlice> {
|
||||
// v1.4.37.1 hotfix — fire-and-forget. See `src/app/api/analytics/route.ts`
|
||||
// for the full rationale: awaiting this on the read path can stall
|
||||
// the event loop for 30–60 s on power-user accounts whose iOS step
|
||||
// samples keep the 90-day window slightly stale. The downstream
|
||||
// the event loop for tens of seconds on tenants with large iOS step
|
||||
// sample sets that keep the 90-day window slightly stale. The downstream
|
||||
// coverage probe falls back to live SQL for uncovered types, so
|
||||
// correctness is preserved; the read just doesn't block waiting
|
||||
// for the background refresh.
|
||||
@@ -250,9 +251,9 @@ export async function computeSummariesSlice(
|
||||
* `(user_id, type, measured_at)` instead of reading every row in the
|
||||
* user's measurements partition. Every FILTER expression inside the
|
||||
* SELECT already restricts to 7/30/90 days, so the cap excludes only
|
||||
* rows that were already being aggregated to NULL. On a power-user
|
||||
* account with ~450k all-time rows this lifts the slim slice from
|
||||
* ~9 s cold to ~0.5-1 s; output is bit-identical.
|
||||
* rows that were already being aggregated to NULL. On tenants with
|
||||
* large measurement partitions this lifts the slim slice from
|
||||
* multi-second cold to sub-second; output is bit-identical.
|
||||
*/
|
||||
async function computeFromRollups(userId: string): Promise<SummariesSlice> {
|
||||
const [narrows, latests, dayBuckets] = await Promise.all([
|
||||
@@ -320,11 +321,12 @@ async function computeFromRollups(userId: string): Promise<SummariesSlice> {
|
||||
// v1.4.37.2 hotfix — the v1.4.35 implementation read EVERY DAY
|
||||
// rollup bucket for the user (`findMany` without a `bucketStart`
|
||||
// window) and then composed `count / min / max / mean` in JS.
|
||||
// On a power-user account that materialised as a 306k-row
|
||||
// transfer + a JS loop = ~3.85 s per cache miss, even with the
|
||||
// rollup table hot. The slim slice's contract is the all-time
|
||||
// count / min / max / mean per type — exactly what a SQL
|
||||
// `GROUP BY type` returns in a single round-trip. Returns 8 rows
|
||||
// On tenants with large measurement partitions that materialised
|
||||
// as a six-figure row transfer + a JS loop = multi-second per
|
||||
// cache miss, even with the rollup table hot. The slim slice's
|
||||
// contract is the all-time count / min / max / mean per type —
|
||||
// exactly what a SQL `GROUP BY type` returns in a single
|
||||
// round-trip. Returns 8 rows
|
||||
// instead of 306k, brings the cache-miss cost into the < 100 ms
|
||||
// budget. The downstream `aggregateBuckets` call is bypassed
|
||||
// because the per-type aggregate is already shaped server-side.
|
||||
@@ -455,7 +457,7 @@ async function computeFromRollups(userId: string): Promise<SummariesSlice> {
|
||||
* table is empty for this user. Subsequent reads pick up the populated
|
||||
* rollup on `computeFromRollups`.
|
||||
*
|
||||
* v1.4.48 M0 — split into two parallel queries:
|
||||
* v1.4.48 — split into two parallel queries:
|
||||
*
|
||||
* 1. `allTime` — the linearly composable columns (`count / min /
|
||||
* max / mean`) keep the full-partition scan because they must
|
||||
@@ -468,8 +470,8 @@ async function computeFromRollups(userId: string): Promise<SummariesSlice> {
|
||||
* cap lets the planner do an index range scan on
|
||||
* `(user_id, type, measured_at)` instead of a full-partition
|
||||
* sequential scan and discarding 95 % of rows inside FILTER
|
||||
* clauses. On a power-user account with ~450k all-time rows the
|
||||
* cold fallback drops from ~9 s to ~0.5-1 s. Output is
|
||||
* clauses. On tenants with large measurement partitions the
|
||||
* cold fallback drops from multi-second to sub-second. Output is
|
||||
* bit-identical because every row excluded by the new outer cap
|
||||
* was already aggregating to NULL inside its FILTER clause.
|
||||
*/
|
||||
@@ -582,7 +584,7 @@ async function computeFromLiveAggregate(
|
||||
};
|
||||
}
|
||||
|
||||
// v1.4.48 M0 — windowed columns are keyed by type alongside the
|
||||
// v1.4.48 — windowed columns are keyed by type alongside the
|
||||
// all-time aggregate. Types that exist in `allTime` but have no
|
||||
// measurements in the 90-day window leave their windowed columns at
|
||||
// `null` (the same shape the pre-split query produced via FILTER
|
||||
|
||||
Reference in new issue
Block a user