chore(release): v1.4.40 — comprehensive Critical+High architecture closure plus iOS PB30 enablement

Eleven touch-disjoint waves landed on develop plus a six-axis QA
reconcile round before tagging. See CHANGELOG.md for the user-facing
prose; this commit message documents the wave + report inventory for
release-management traceability.

Waves shipped (each with a phase report under .planning/):

- phase-W-POOL-v1440-report.md — p-limit(4) wrapper around the
  analytics 15-way fan-out + pg.Pool max raised 10 -> 20. Closes the
  v1.4.39 empirical-trace finding #1 (Prisma pool starvation).
- phase-W-DELETED-v1440-report.md — 11 reader-tier helpers + 3
  integration contracts close audit Critical Finding #3 (soft-delete
  invisibility) across dashboard, analytics, summaries, rollup tiers,
  and the Coach snapshot path.
- phase-W-INSIGHTS-v1440-report.md — mood-rollup swap on the three
  /api/insights/{features,targets,cards} routes plus the
  compliance-rollup hook gap closure for bulk-projection mints; the
  post-QA reconcile patch closes the six remaining measurement.findMany
  sites in the cluster that still leaked tombstoned rows.
- phase-W-INFRA-v1440-report.md — rollup helper umbrella consolidation
  under src/lib/rollups/* with zero orphan re-exports; lint, typecheck,
  knip, and 93/93 rollup tests green post-restructure.
- phase-W-RSC-v1440-report.md — per-tile Suspense boundaries plus the
  queryKey factory-bypass walker test (extended to comparison/ in the
  reconcile pass).
- phase-W-PRIVACY-v1440-report.md — bilingual /privacy page with 17
  paired-section tests pinning the 9 SB-3 disclosure requirements.
- phase-W-AASA-v1440-report.md — /.well-known/apple-app-site-association
  endpoint pinned to S8WDX4W5KX.dev.healthlog.app with app-ID parity
  assertions guarding against split rotation.
- phase-W-APNS-NOTIFY-v1440-report.md — MEDICATION_REMINDER
  time-sensitive APNs payload (priority 10 + interruption-level)
  scoped via a parameterised test that pins all 6 other event-types
  do not bypass Focus. AP-2 .p8 install gates real delivery.
- phase-W-CONSENT-v1440-report.md — AI consent receipts CRUD with
  append-only invariant pinned in test; revoke-then-regrant leaves
  both rows; idempotent DELETE returns 200 + receipt: null.
- phase-W-WMY-WIRE-v1440-report.md — three previously-dead WMY readers
  (week/month/year) wired through summaries-slice and
  health-score-fast-path with a linear-composability parity test
  pinning the cross-consumer routing contract.
- phase-W-GHOSTS-v1440-report.md — ~1 177 lines of dead code removed
  across 9 atomic commits; startOfUtcDay + wallClockInTz consolidation
  eliminates a sibling-drift risk; knip whitelist trimmed to three
  files pending follow-up.

QA reconcile patches applied per the v1.4.40 QA round (six reports
under .planning/round-v1440-QA-*-findings.md):

- senior C1 — dashboard-suspense-boundaries regex aligned with the
  post-W-INFRA Thread 2 userTimezone-local lift.
- senior H1 — soft-delete filter added to the six remaining
  insights measurement.findMany sites.
- specialist H1 — Promise.allSettled around bulk-projection
  compliance recompute on both /api/medications/intake and
  /api/dashboard/summary.
- ux H1 — compare-toggle migrated to queryKeys.dashboardWidgets
  factory; walker test extended to src/components/comparison/.
- security M1 — consent artefact 64 KB cap enforced via
  Buffer.byteLength (UTF-8 bytes, not UTF-16 code units).

Quality gates green at tag: pnpm typecheck, pnpm lint, pnpm knip
--include files,dependencies,binaries,unlisted, pnpm test --run
(4726 passing / 1 skipped). Tests 4525 -> 4726 unit + 1 skipped over
the v1.4.39 -> v1.4.40 span.

User-facing prose: see CHANGELOG.md.
This commit is contained in:
Marc-André Bombeck committed 2026-05-21 12:48:02 +02:00
1 parent f9996552ed
commit 1114cf7e63
146 files changed
+8207 -2279

No files matched your search

+8 -3
View File
@@ -2,6 +2,7 @@
import { useQuery } from "@tanstack/react-query";
import { useAuth } from "@/hooks/use-auth";
import { queryKeys } from "@/lib/query-keys";
import {
ResponsiveContainer,
ComposedChart,
@@ -541,8 +542,12 @@ export function HealthChart({
}, [rangePoints, effectiveCompareBaseline]);
const { data, isLoading } = useQuery({
queryKey: [
"chart-data",
// v1.4.40 W-RSC — route the chart-data key through `queryKeys.chartData`
// so the `["chart-data"]` prefix lands in `measurementDependentKeys`
// and a fresh measurement evicts every per-chart daily cache in one
// pass (audit C2). The factory-packed tuple is byte-identical with
// the pre-v1.4.40 inline literal so the cache layout stays stable.
queryKey: queryKeys.chartData(
types.join(","),
valueMode,
bmiDivisor ?? "no-bmi",
@@ -555,7 +560,7 @@ export function HealthChart({
// re-running the unbounded walk.
fetchWindow.from,
fetchWindow.to,
],
),
// v1.4.28 FB-D2 — cache the bounded window for a minute so tab
// navigation between insights sub-pages does not re-fire every
// chart's fetch. `gcTime` keeps the inactive cache for five
@@ -34,6 +34,7 @@
*/
import { useQuery } from "@tanstack/react-query";
import { useMemo, useState } from "react";
import { queryKeys } from "@/lib/query-keys";
import {
ResponsiveContainer,
ComposedChart,
@@ -226,8 +227,12 @@ export function MedicationComplianceChart({
// room of the weight / BMI charts.
const viewportWidth = useViewportWidth();
// v1.4.40 W-RSC — factory-routed key so the prefix
// `["dashboard-medication-compliance"]` lands in
// `medicationDependentKeys` and an intake POST refreshes the chart
// immediately rather than waiting for `staleTime` (audit L4).
const { data, isLoading } = useQuery({
queryKey: ["medication-compliance-chart", days],
queryKey: queryKeys.dashboardMedicationCompliance(days),
queryFn: async (): Promise<DailyCompliancePoint[]> => {
const res = await fetch(
`/api/medications/intake?scope=compliance&days=${days}`,
+18 -1
View File
@@ -2,6 +2,7 @@
import { useQuery } from "@tanstack/react-query";
import { useAuth } from "@/hooks/use-auth";
import { queryKeys } from "@/lib/query-keys";
import {
ResponsiveContainer,
ComposedChart,
@@ -312,8 +313,22 @@ export function MoodChart({
const displayTitle = title ?? t("charts.mood");
// v1.4.40 W-RSC — align the chart's queryKey with the dashboard's
// `useQuery({ queryKey: queryKeys.moodAnalytics() })` call against the
// same `/api/mood/analytics` endpoint. Pre-fix the chart fetched its
// own copy under `["mood-chart-data"]` (audit C1) — TanStack treated
// the two keys as separate cache slots and the cold-mount dashboard
// fired the request twice (once at `app/page.tsx:291`, once here on
// the chart's first render). The dedup eliminates a ~22 KB round-trip
// and frees one pg pool slot per cold mount.
//
// `staleTime: 60_000` matches the dashboard's `DASHBOARD_QUERY_OPTS`
// so a route swing back to the dashboard within a minute keeps the
// cache fresh; the chart's own subscription becomes the cache writer
// for any insights / mood sub-page that mounts the chart without the
// dashboard parent having fetched first.
const { data, isLoading } = useQuery({
queryKey: ["mood-chart-data"],
queryKey: queryKeys.moodAnalytics(),
queryFn: async (): Promise<MoodAnalyticsData> => {
const res = await fetch("/api/mood/analytics");
if (!res.ok) throw new Error("Failed to fetch mood analytics");
@@ -321,6 +336,8 @@ export function MoodChart({
return json.data as MoodAnalyticsData;
},
enabled: isAuthenticated,
staleTime: 60_000,
refetchOnWindowFocus: false,
});
const chartData = useMemo((): ChartDataPoint[] | undefined => {
+11 -9
View File
@@ -3,6 +3,7 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useTranslations } from "@/lib/i18n/context";
import { cn } from "@/lib/utils";
import { queryKeys } from "@/lib/query-keys";
import {
COMPARISON_BASELINES,
resolveDashboardLayout,
@@ -26,7 +27,7 @@ export function CompareToggle({ className }: { className?: string }) {
const queryClient = useQueryClient();
const { data: layoutData } = useQuery({
queryKey: ["user", "dashboardWidgets"],
queryKey: queryKeys.dashboardWidgets(),
queryFn: async () => {
const res = await fetch("/api/dashboard/widgets");
if (!res.ok) throw new Error("failed");
@@ -52,15 +53,14 @@ export function CompareToggle({ className }: { className?: string }) {
},
onMutate: async (next) => {
await queryClient.cancelQueries({
queryKey: ["user", "dashboardWidgets"],
queryKey: queryKeys.dashboardWidgets(),
});
const prev = queryClient.getQueryData<DashboardLayout>([
"user",
"dashboardWidgets",
]);
const prev = queryClient.getQueryData<DashboardLayout>(
queryKeys.dashboardWidgets(),
);
if (prev) {
queryClient.setQueryData<DashboardLayout>(
["user", "dashboardWidgets"],
queryKeys.dashboardWidgets(),
{ ...prev, comparisonBaseline: next },
);
}
@@ -68,11 +68,13 @@ export function CompareToggle({ className }: { className?: string }) {
},
onError: (_err, _next, ctx) => {
if (ctx?.prev) {
queryClient.setQueryData(["user", "dashboardWidgets"], ctx.prev);
queryClient.setQueryData(queryKeys.dashboardWidgets(), ctx.prev);
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["user", "dashboardWidgets"] });
queryClient.invalidateQueries({
queryKey: queryKeys.dashboardWidgets(),
});
},
});
@@ -58,7 +58,7 @@ import {
* shaped the same as `MoodForm`).
*
* On success: invalidate `medicationDependentKeys` (medications +
* analytics + insights + intake-summary + achievements) PLUS the
* analytics + insights + achievements) PLUS the
* inline per-medication compliance chart key so the detail page tile
* refreshes if it happens to be mounted elsewhere in the app.
*
@@ -9,6 +9,7 @@ import { useWorkouts } from "@/hooks/use-workouts";
import { InsightsTabStrip } from "@/components/insights/insights-tab-strip";
import { useInsightsAdvisorQuery } from "@/components/insights/use-insights-advisor";
import { useAnalyticsQuery } from "@/lib/queries/use-analytics-query";
import { queryKeys } from "@/lib/query-keys";
import type { InsightInputs } from "@/lib/insights/metric-availability";
/**
@@ -66,7 +67,7 @@ export function InsightsLayoutShell({ children }: { children: ReactNode }) {
// event-driven gating branches. Sub-pages read the same key so the
// payload lands once per route.
const comprehensiveQuery = useQuery({
queryKey: ["insights", "comprehensive"],
queryKey: queryKeys.insightsComprehensive(),
queryFn: async () => {
const res = await fetch("/api/insights/comprehensive");
if (!res.ok) throw new Error("Failed");