v1.4.33 — polish and reliability

Quality-leap release between two HealthKit milestones. The headline is a P0 hotfix for the `/api/analytics` 500 that broke the Insights mother page for any account with more than a few thousand measurements: the six per-metric status helpers were spreading the entire numeric history into `Math.min` / `Math.max`, which trips a stack overflow once a single argument list exceeds the V8 spread cap. Both the surfaced symptom and the latent risk in five sibling helpers were folded onto a reduce-based min/max path.

Around that hotfix landed a deep polish pass: the Insights mother page now defers three below-the-fold blocks behind `next/dynamic` to trim the cold mount cost, the analytics endpoint gained a `?slice=summaries` slim slice that the per-metric sub-pages now ride, the Coach snapshot builder picked up a 60 s LRU keyed on `(userId, scope)`, the Settings shell consolidated three duplicate copies of the about / notifications / about-dropdown surfaces, the navigation strip dropped a redundant "Home" group label and a colliding "Notifications" sibling, every icon-only button on the medications and admin surfaces gained an accessible name, every Progress bar gained one too, three pages had their heading hierarchy repaired to a sequential h1 → h2 → h3, the mobile Coach FAB now hides while a chart tooltip is open so it cannot occlude the read-out, and the auth shell normalised every authenticated route to a single `max-w-screen-xl` container.

Also lands the Apple App Site Association handler at `/.well-known/apple-app-site-association` on every host that fronts the app, advertising the iOS bundle's App ID prefix (S8WDX4W5KX.dev.healthlog.app) under `webcredentials.apps` so the passkey ceremony shares cleanly between the web origin and the iOS app.

The release closes nineteen issues from a five-surface audit + a runtime bug-hunt pass, plus six follow-up tracks of polish across the affected surfaces.
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-05-16 19:46:43 +02:00
1 parent c7a0ff214b
commit 7bef199039
186 files changed
+9358 -946

No files matched your search

+12 -18
View File
@@ -1,10 +1,8 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { useAuth } from "@/hooks/use-auth";
import { hasMetricData, type InsightMetric } from "@/lib/insights/metric-availability";
import { queryKeys } from "@/lib/query-keys";
import { useAnalyticsQuery } from "@/lib/queries/use-analytics-query";
import type { SubPageAnalyticsData } from "@/types/analytics";
/**
@@ -53,24 +51,20 @@ export function useInsightsAnalytics(
metric: InsightMetric,
): UseInsightsAnalyticsResult {
const { isAuthenticated } = useAuth();
const query = useQuery({
queryKey: queryKeys.analytics(),
queryFn: async (): Promise<SubPageAnalyticsData> => {
const res = await fetch("/api/analytics");
if (!res.ok) throw new Error("Failed");
const json = (await res.json()) as { data: SubPageAnalyticsData };
return json.data;
},
enabled: isAuthenticated,
staleTime: 60 * 1000,
});
// v1.4.33 IW2 — sub-pages only read `summaries[METRIC].count` for the
// gating short-circuit, so they ride on the slim slice. The mother
// page keeps the default thick slice for `correlations` /
// `healthScore` / `bpInTargetPct*`; both consumers live in the same
// queryKey tree (root `["analytics"]`) but the slim slice has its own
// cache slot under `["analytics", "summaries"]`.
const query = useAnalyticsQuery({ slice: "summaries" });
const data = query.data as SubPageAnalyticsData | undefined;
const isEmpty =
Boolean(isAuthenticated) &&
query.data !== undefined &&
data !== undefined &&
!hasMetricData(metric, {
summaries: query.data.summaries,
summaries: data.summaries,
// Sub-pages that go through this hook are sensor-backed only
// (PULSE / WEIGHT / BMI / BLOOD_PRESSURE_SYS / SLEEP_DURATION).
// Mood + medication run their own gate.
@@ -79,7 +73,7 @@ export function useInsightsAnalytics(
});
return {
data: query.data,
data,
isLoading: query.isLoading,
isEmpty,
error: query.error as Error | null,
+34
View File
@@ -0,0 +1,34 @@
"use client";
import { useEffect } from "react";
/**
* v1.4.33 IW9 — single source of truth for the "scroll to top on
* route mount" affordance.
*
* The pre-v1.4.33 wiring had `<SubPageShell>` AND the insights mother
* page each firing their own `requestAnimationFrame → scrollTo({top:0})`
* on mount. Two consumers doing the same trick is the kind of
* duplication that drifts; the second `scrollTo` lands ~16 ms after the
* first and re-snaps the viewport once the chart skeleton has inflated.
* On slow hydrates the user sees a visible "jump" — exactly the
* complaint in `.planning/round-v1433-audit-polish.md` §2.1.
*
* This hook is the single owner of that reset. Both the shell and the
* mother page call into it; the effect is mount-only and uses
* `requestAnimationFrame` so the scroll lands after the first paint
* (chart skeleton heights have settled, no double-snap).
*
* `behavior: "auto"` is intentional — `prefers-reduced-motion` users
* get the same instant reset everyone else does. A smooth scroll on
* a route change is itself a motion that some users opt out of.
*/
export function useScrollResetOnRoute() {
useEffect(() => {
if (typeof window === "undefined") return;
const handle = window.requestAnimationFrame(() => {
window.scrollTo({ top: 0, behavior: "auto" });
});
return () => window.cancelAnimationFrame(handle);
}, []);
}