mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
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:
1 parent
f9996552ed
commit
1114cf7e63
146 files changed
+8207
-2279
No files matched your search
@@ -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}`,
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user