v1.4.39.x stitched the rollup tier across mood, medication compliance, and cumulative metrics, and closed the dashboard read paths the rollup tier replaced. v1.4.40 is the architecture-closure release on top of that base: the Prisma pool starvation root cause documented in the v1.4.39 empirical trace is fixed at the source, the soft-delete invisibility contract is now consistent across every reader tier, the per-tile Suspense boundaries that let dashboard chart tiles stream independently are in place, the queryKey factory has CI enforcement that catches bare-literal regressions, and the iOS PB30 backend prerequisites (Apple App-Site Association, AI consent receipts CRUD, time-sensitive APNs payload, public privacy page, notifications/status surface) are live so the iOS v0.5.x sprint can land its dependent screens without further backend churn.
Added
POST /api/consent/ai+GET /api/consent/ai/latest+DELETE /api/consent/ai— AI consent receipts for App-Store Guideline 5.1.2(i) and GDPR Art. 7 audit trail. Discriminated bykindoverai_full / ai_insights_only / ai_coachso each consent surface the iOS client collects independently ends up as its own row. Append-only: revoking a receipt writes a new row, never mutates history. 64 KB byte-bounded artefact cap (UTF-8 byte length, not character count) keeps the audit table from absorbing multi-megabyte rows from a misbehaving client.GET /.well-known/apple-app-site-association— universal-link AASA payload pinned toS8WDX4W5KX.dev.healthlog.app. Response is served asapplication/jsonwithout a charset parameter (Apple'sswcddaemon is strict about the Content-Type shape) and the payload structure is regression-tested for app-ID parity againstapplinks.details[].appIDs,webcredentials.apps, andappclips.appsso a future split rotation cannot drift one bundle out of the trio.GET /privacy— public bilingual (German / English) privacy page covering the nine disclosure requirements (data categories, third-party processors, sub-processors, retention, deletion rights, AI-assistance scope, marketing posture, contact). Paired-section layout (no JS-driven locale switching) so the static document remains comprehensible to a regulator regardless of the browser'sAccept-Language.GET /api/notifications/status— operator-and-iOS-side surface for the APNs reachability ledger. Reportsaps_ready / aps_last_error / aps_last_delivery_at / device_token_countso the iOS app can flag a stale-token failure mode without a separate diagnostic call.- CI knip gate (
.github/workflows/knip.yml) — fails any push tomaincarrying unused exports, unlisted dependencies, or orphaned binaries. The whitelist (knip.json) carries three files deferred pending the "delete with their dedicated tests" follow-up; everything else is green. src/lib/rollups/umbrella — every rollup helper now lives under one canonical import root. The previous in-tree split betweensrc/lib/measurements/rollups*,src/lib/mood/mood-rollups*, andsrc/lib/medications/medication-compliance-rollups*is collapsed into@/lib/rollups/{measurement,mood,medication-compliance,read-wmy,read-cumulative}.ts. Importers updated in lockstep; zero orphan re-exports.
Changed
- Per-tile Suspense boundaries on the dashboard. Each chart tile now mounts inside its own
<Suspense>so a slow tile no longer blocks the others on first paint. The parent gate is the slim / thick analytics merge (already split per v1.4.39.2); the per-tile Suspense layer is the structural foundation the v1.4.41 React Server Components migration will graft onto.mood-chartqueryKey dedup eliminates one round-trip on cold mount. - queryKey factory enforcement. A walker test fails CI if any guarded file declares a bare-literal
queryKey: [...]. Guarded roots aresrc/components/charts/,src/components/comparison/,src/app/page.tsx, andsrc/hooks/use-auth.ts. Cheaper than a custom ESLint rule and points the failure message at the exactfile:linea contributor needs to fix; opt-in expansion as future surfaces migrate the remaining sites away from bare literals.
Fixed
- Prisma pool starvation root cause. The 15-way
fetchMeasurementSeriesChunkedfan-out in/api/analyticsthick used to monopolise at least 8 of the default-10pg.Poolconnections during a power-user cold mount, blocking every secondary chart-tile fetch behind it. The fan-out is now wrapped inp-limit(4)so analytics holds at most 4 pool slots, and thepg.Poolmaxis raised from the library default 10 to 20 (overridable viaDATABASE_POOL_MAX) so a second concurrent power-user retains at least 8 free slots after both branches hit theirp-limit(4)cap. The cap is a per-request instance, not module-level, so a stale limit cannot leak in-flight state across HTTP boundaries. - Soft-delete invisibility full-wire. Eleven reader-tier helpers across
src/lib/measurements/rollups.ts,src/lib/measurements/rollup-coverage.ts,src/lib/analytics/{summaries-slice,correlations-fast-path,bp-in-target-fast-path,health-score-fast-path}.ts,src/lib/insights/comprehensive-aggregator.ts,src/app/api/dashboard/summary/route.ts,src/app/api/measurements/route.ts,src/app/api/measurements/series/route.ts, andsrc/lib/ai/coach/snapshot.tsnow filterdeletedAt: null(or the SQL equivalentm."deleted_at" IS NULL) at every aggregate, every cursor walk, everyDISTINCT ONlatest probe, and every rollup-rebuild SQL. Three integration-test contracts intests/integration/measurement-soft-delete.test.tspin the invariant against a Postgres testcontainer. - Six remaining insights
measurement.findManysites that the mood-rollup swap left unfiltered (/api/insights/{targets,cards,generate}plussrc/lib/insights/{features,glp1-plateau,pulse-status}.ts). All six now filterdeletedAt: nullso the iOS-adapter card stream, the prompt feature aggregator, the GLP-1 plateau detector window, and the per-type tile-strip averages stop counting tombstoned readings once iOS sync starts emitting deletions. - Compliance-rollup hook gap on bulk-projection paths. Both
/api/medications/intake?scope=todayand/api/dashboard/summarymint fresh(medicationId, scheduledFor)rows in PENDING state when a daily schedule is projected on first read. Without a recompute hook the rollup for the affected(user, medication, day)tuples stayed at its previous (pre-projection)scheduledcount, which inflated the apparent compliance % until the user logged against the new row. Both call sites now fire one recompute per distinct(medicationId, dayKey)tuple, deduplicated through aSetso the cost stays bounded. The recompute call is wrapped inPromise.allSettledso any future change that lets the helper throw still leaves the user request 200-OK. /api/dashboard/summarynested-ternary regression in the heroNumber branch flattened to anif / else if / elsechain so the linter, the type-narrower, and a human reader all parse the same way.- Lint regression in
src/lib/rollups/(post-umbrella-move) — a strayanyimport path and one un-narrowedunknownresolved by the typecheck-led restructure. dashboard-suspense-boundaries.test.tsregex pin updated to the post-restructure shape. The test pinneduseMemo(..., [user?.timezone])but the production code liftsuser?.timezoneto auserTimezonelocal one line above theuseMemoso the dependency array stays a stable reference across renders.- Consent artefact 64 KB cap enforced via
Buffer.byteLength(value, "utf8")(not the priorz.string().max()which counts UTF-16 code units). A UTF-8 artefact full of multi-byte code points would have slipped past the 64 KB row budget; the audit-table guarantee is byte-bounded, not code-unit-bounded.
Performance
Expected on power-user accounts; numbers anchored on the v1.4.40 empirical trace and instrumentation captured during the release work. Live perf-verify rides the post-deploy window.
- Chart-tile first-paint: +7.3 s → +1.6 s. Bounded analytics fan-out (p-limit 4 + pool max 20) lets the 6x
/api/measurements?source=rollupburst release incrementally as analytics rotates lanes, instead of gating the entire burst behind the thick analytics drain. - Six insights routes cold-mount. The mood-rollup swap on
/api/insights/{features,targets,cards}moves mood aggregation off the liveMoodEntry.findManywalk onto the v1.4.39 mood-rollup tier; the cold-mount budget for the affected routes drops onto the same flat-200 ms band the v1.4.39/api/mood/analyticsnumbers land in. avg30LastYearnow populated. The 425-daysincecap on the/api/analyticslive-fallback per-type loop (v1.4.39) lets the year-ago baseline tile resolve from raw data without forcing a 347 k-row scan; the v1.4.40 pool cap keeps that fallback off the hot path.slope90via MONTH-tier reader.readBestGranularityRollupsauto-routes the 90-day slope window onto the MONTH bucket where coverage allows, eliminating a per-cold-mount aggregate on multi-year tenants.
Operator notes
- Migration 0074 adds the
consent_receiptstable (id, userId, kind, artefact, signedAt, revokedAt, createdAt) and the matching index over(userId, kind, revokedAt, signedAt DESC). Additive; no destructive column drops; safe to run forward on a live database. - APNs
.p8key install gates real time-sensitive delivery. Thetime-sensitive + priority 10payload landed in the worker (MEDICATION_REMINDERonly — the parameterised test pins all six other event-types do not bypass Focus) but real delivery requires the production.p8private key to be installed in the Coolify secret store. Until that key lands,aps_last_errorwill surface asauth-failedon/api/notifications/status— that is the expected pre-key state, not a regression of this release. - No breaking API contract change for iOS v0.5.4. Every existing route shape is byte-compatible; the new routes (
/api/consent/ai*,/.well-known/apple-app-site-association,/api/notifications/status,/privacy) are additive surfaces. - No env-var change required for upgrade.
DATABASE_POOL_MAXis optional (defaults to 20). pnpm test --rungreen at 4726 passing / 1 skipped (4727 total);pnpm typecheck,pnpm lint,pnpm knip --include files,dependencies,binaries,unlistedall green; the knip CI gate now fails any push tomaincarrying unused exports.