github MBombeck/HealthLog v1.4.40
v1.4.40 — Critical+High architecture closure + iOS PB30 enablement

one hour ago

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 by kind over ai_full / ai_insights_only / ai_coach so 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 to S8WDX4W5KX.dev.healthlog.app. Response is served as application/json without a charset parameter (Apple's swcd daemon is strict about the Content-Type shape) and the payload structure is regression-tested for app-ID parity against applinks.details[].appIDs, webcredentials.apps, and appclips.apps so 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's Accept-Language.
  • GET /api/notifications/status — operator-and-iOS-side surface for the APNs reachability ledger. Reports aps_ready / aps_last_error / aps_last_delivery_at / device_token_count so 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 to main carrying 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 between src/lib/measurements/rollups*, src/lib/mood/mood-rollups*, and src/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-chart queryKey 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 are src/components/charts/, src/components/comparison/, src/app/page.tsx, and src/hooks/use-auth.ts. Cheaper than a custom ESLint rule and points the failure message at the exact file:line a 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 fetchMeasurementSeriesChunked fan-out in /api/analytics thick used to monopolise at least 8 of the default-10 pg.Pool connections during a power-user cold mount, blocking every secondary chart-tile fetch behind it. The fan-out is now wrapped in p-limit(4) so analytics holds at most 4 pool slots, and the pg.Pool max is raised from the library default 10 to 20 (overridable via DATABASE_POOL_MAX) so a second concurrent power-user retains at least 8 free slots after both branches hit their p-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, and src/lib/ai/coach/snapshot.ts now filter deletedAt: null (or the SQL equivalent m."deleted_at" IS NULL) at every aggregate, every cursor walk, every DISTINCT ON latest probe, and every rollup-rebuild SQL. Three integration-test contracts in tests/integration/measurement-soft-delete.test.ts pin the invariant against a Postgres testcontainer.
  • Six remaining insights measurement.findMany sites that the mood-rollup swap left unfiltered (/api/insights/{targets,cards,generate} plus src/lib/insights/{features,glp1-plateau,pulse-status}.ts). All six now filter deletedAt: null so 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=today and /api/dashboard/summary mint 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) scheduled count, 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 a Set so the cost stays bounded. The recompute call is wrapped in Promise.allSettled so any future change that lets the helper throw still leaves the user request 200-OK.
  • /api/dashboard/summary nested-ternary regression in the heroNumber branch flattened to an if / else if / else chain 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 stray any import path and one un-narrowed unknown resolved by the typecheck-led restructure.
  • dashboard-suspense-boundaries.test.ts regex pin updated to the post-restructure shape. The test pinned useMemo(..., [user?.timezone]) but the production code lifts user?.timezone to a userTimezone local one line above the useMemo so the dependency array stays a stable reference across renders.
  • Consent artefact 64 KB cap enforced via Buffer.byteLength(value, "utf8") (not the prior z.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=rollup burst 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 live MoodEntry.findMany walk 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/analytics numbers land in.
  • avg30LastYear now populated. The 425-day since cap on the /api/analytics live-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.
  • slope90 via MONTH-tier reader. readBestGranularityRollups auto-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_receipts table (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 .p8 key install gates real time-sensitive delivery. The time-sensitive + priority 10 payload landed in the worker (MEDICATION_REMINDER only — the parameterised test pins all six other event-types do not bypass Focus) but real delivery requires the production .p8 private key to be installed in the Coolify secret store. Until that key lands, aps_last_error will surface as auth-failed on /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_MAX is optional (defaults to 20).
  • pnpm test --run green at 4726 passing / 1 skipped (4727 total); pnpm typecheck, pnpm lint, pnpm knip --include files,dependencies,binaries,unlisted all green; the knip CI gate now fails any push to main carrying unused exports.

Don't miss a new HealthLog release

NewReleases is sending notifications on new releases.