mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
perf(insights,api): always answer instantly and regenerate in the background
The insights read path served a cold cache by generating inline — up to ten seconds on first visit — and the nightly warm pass structurally excluded evening visitors: their visit stamped the cache, the half-four run skipped it as fresh, and it expired exactly at the next evening's visit. Stale content now serves immediately with its timestamp while one background job revalidates, the warm threshold drops to an hour, forced warming fills both message locales, the analytics caches serve stale-while-revalidate, the profile endpoint parallelises its reads, the bug-report status drops a write per call, the umami proxy degrades to a no-op script instead of an error, the insights route sheds its heaviest static imports, and the medication page prefetches on intent.
This commit is contained in:
1 parent
ceeb818f41
commit
78fe82e348
19 files changed
+319
-101
No files matched your search
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* v1.16.7 — single async boundary for the three mood mini-charts.
|
||||
*
|
||||
* The distribution / weekday / time-of-day charts were each deferred
|
||||
* through their own `next/dynamic` import, which produced three separate
|
||||
* chunk groups: the cards popped in one after another as each chunk
|
||||
* landed (incoherent reveal on the mood insights page), and each group
|
||||
* carried its own copy of the Recharts module graph. Funnelling all
|
||||
* three through this one barrel gives them a single shared chunk — they
|
||||
* arrive (and reveal) together, and Recharts is bundled once for the
|
||||
* trio.
|
||||
*
|
||||
* Consumers keep importing the chart types directly from the component
|
||||
* files (type-only imports are value-free and don't drag the chunk in).
|
||||
*/
|
||||
export { MoodDistributionChart } from "./mood-distribution-chart";
|
||||
export { MoodWeekdayChart } from "./mood-weekday-chart";
|
||||
export { MoodTimeOfDayChart } from "./mood-time-of-day-chart";
|
||||
@@ -36,9 +36,12 @@ import { MoodHeatmap } from "@/components/charts/mood-heatmap";
|
||||
import type { MoodDistributionRow } from "./mood-distribution-chart";
|
||||
import type { MoodWeekdayRow } from "./mood-weekday-chart";
|
||||
import type { MoodTimeOfDayPattern } from "./mood-time-of-day-chart";
|
||||
// v1.16.7 — all three loaders resolve the ONE `./mood-charts` barrel so
|
||||
// they share a single chunk: the cards reveal together instead of
|
||||
// popping in chunk-by-chunk, and Recharts ships once for the trio.
|
||||
const MoodDistributionChart = dynamic(
|
||||
() =>
|
||||
import("./mood-distribution-chart").then((mod) => ({
|
||||
import("./mood-charts").then((mod) => ({
|
||||
default: mod.MoodDistributionChart,
|
||||
})),
|
||||
{
|
||||
@@ -48,7 +51,7 @@ const MoodDistributionChart = dynamic(
|
||||
);
|
||||
const MoodWeekdayChart = dynamic(
|
||||
() =>
|
||||
import("./mood-weekday-chart").then((mod) => ({
|
||||
import("./mood-charts").then((mod) => ({
|
||||
default: mod.MoodWeekdayChart,
|
||||
})),
|
||||
{
|
||||
@@ -58,7 +61,7 @@ const MoodWeekdayChart = dynamic(
|
||||
);
|
||||
const MoodTimeOfDayChart = dynamic(
|
||||
() =>
|
||||
import("./mood-time-of-day-chart").then((mod) => ({
|
||||
import("./mood-charts").then((mod) => ({
|
||||
default: mod.MoodTimeOfDayChart,
|
||||
})),
|
||||
{
|
||||
|
||||
@@ -3,11 +3,16 @@
|
||||
import { useCallback } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { InsightResult } from "@/lib/ai/types";
|
||||
import {
|
||||
dailyBriefingSchema,
|
||||
trendAnnotationsSchema,
|
||||
type DailyBriefing as DailyBriefingPayload,
|
||||
type TrendAnnotations,
|
||||
// Type-only — the runtime schemas load lazily inside `fetchAdvisor` so
|
||||
// the zod module graph (`@/lib/ai/schema` is ~550 lines of zod builders)
|
||||
// stays out of the insights route-entry chunk. This hook is imported
|
||||
// eagerly by BOTH the insights page and the layout shell, so a value
|
||||
// import here landed zod in the chunk every insights visit downloads
|
||||
// before first paint; the schemas are only needed once a payload has
|
||||
// actually arrived, which is by definition after the network hop.
|
||||
import type {
|
||||
DailyBriefing as DailyBriefingPayload,
|
||||
TrendAnnotations,
|
||||
} from "@/lib/ai/schema";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { apiFetchRaw } from "@/lib/api/api-fetch";
|
||||
@@ -157,6 +162,12 @@ async function fetchAdvisor(
|
||||
}
|
||||
const json = await res.json();
|
||||
const payload = json.data as InsightAdvisorPayload;
|
||||
// Lazy schema load — see the type-only import note at the top. The
|
||||
// module is cached after the first call, so this await is free from
|
||||
// the second fetch on.
|
||||
const { dailyBriefingSchema, trendAnnotationsSchema } = await import(
|
||||
"@/lib/ai/schema"
|
||||
);
|
||||
// The cached `insights` blob may carry a `dailyBriefing` from a fresh
|
||||
// PROMPT_VERSION 4.20.x generation. Lift it onto the payload so
|
||||
// consumers don't have to know the legacy shape.
|
||||
|
||||
@@ -41,14 +41,20 @@ vi.mock("@/hooks/use-auth", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { BottomNav } from "../bottom-nav";
|
||||
|
||||
function render() {
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">
|
||||
<BottomNav />
|
||||
</I18nProvider>,
|
||||
// The nav reads `useQueryClient()` for the medications intent
|
||||
// prefetch (v1.16.7); a fresh client per render keeps the SSR
|
||||
// markup assertions isolated.
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<I18nProvider initialLocale="en">
|
||||
<BottomNav />
|
||||
</I18nProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ vi.mock("@/components/app-settings-provider", () => ({
|
||||
useAppSettings: () => mockSettingsRef.value,
|
||||
}));
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { SidebarNav } from "../sidebar-nav";
|
||||
import { ADMIN_SECTIONS } from "@/components/admin/admin-shell";
|
||||
@@ -66,9 +67,14 @@ function render({
|
||||
mockSettingsRef.value = { bugReportEnabled };
|
||||
mockUserRef.value = { ...mockUserRef.value, role, cycleTrackingEnabled };
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">
|
||||
<SidebarNav />
|
||||
</I18nProvider>,
|
||||
// The nav reads `useQueryClient()` for the medications intent
|
||||
// prefetch (v1.16.7); a fresh client per render keeps the SSR
|
||||
// markup assertions isolated.
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<I18nProvider initialLocale="en">
|
||||
<SidebarNav />
|
||||
</I18nProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { medicationsPrefetchIntentProps } from "@/lib/queries/prefetch-medications";
|
||||
import { useState } from "react";
|
||||
import { useAppSettings } from "@/components/app-settings-provider";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
@@ -96,6 +98,10 @@ export function BottomNav() {
|
||||
const pathname = usePathname();
|
||||
const { t } = useTranslations();
|
||||
const { user } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
// v1.16.7 — touch intent on the medications tab starts the list
|
||||
// request before the navigation commits (see sidebar-nav).
|
||||
const medsIntent = medicationsPrefetchIntentProps(queryClient);
|
||||
const { bugReportEnabled } = useAppSettings();
|
||||
const [moreOpen, setMoreOpen] = useState(false);
|
||||
const [captureOpen, setCaptureOpen] = useState(false);
|
||||
@@ -132,6 +138,7 @@ export function BottomNav() {
|
||||
href={item.href}
|
||||
aria-label={t(item.tKey)}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
{...(item.href === "/medications" ? medsIntent : {})}
|
||||
// Touch target sized to WCAG 2.5.5 (44×44 CSS px). The
|
||||
// outer min-h-11 min-w-11 is the actual hit area; the icon
|
||||
// stays visually centered at 20px so the design doesn't shift.
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { medicationsPrefetchIntentProps } from "@/lib/queries/prefetch-medications";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Logo } from "@/components/ui/logo";
|
||||
import { useAuth, useLogout } from "@/hooks/use-auth";
|
||||
@@ -267,6 +269,11 @@ export function SidebarNav() {
|
||||
const pathname = usePathname();
|
||||
const { t } = useTranslations();
|
||||
const { user } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
// v1.16.7 — hover / focus intent on the medications link starts the
|
||||
// list request before the navigation commits, so the due-time cells
|
||||
// hydrate from a warm cache instead of serialising behind the chunk.
|
||||
const medsIntent = medicationsPrefetchIntentProps(queryClient);
|
||||
const { bugReportEnabled } = useAppSettings();
|
||||
const isAdmin = user?.role === "ADMIN";
|
||||
// Match `/admin` exactly or any `/admin/...` sub-route for active-link
|
||||
@@ -407,6 +414,7 @@ export function SidebarNav() {
|
||||
href={item.href}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
data-tour-id={item.tourId}
|
||||
{...(item.href === "/medications" ? medsIntent : {})}
|
||||
className={cn(
|
||||
"flex items-center justify-center rounded-lg p-2.5 transition-colors",
|
||||
isActive
|
||||
@@ -430,6 +438,7 @@ export function SidebarNav() {
|
||||
href={item.href}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
data-tour-id={item.tourId}
|
||||
{...(item.href === "/medications" ? medsIntent : {})}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",
|
||||
isActive
|
||||
|
||||
@@ -21,6 +21,7 @@ import { AppSettingsProvider } from "@/components/app-settings-provider";
|
||||
import { VersionPoller } from "@/components/version-poller";
|
||||
import { isDashboardSnapshotEnabled } from "@/lib/dashboard/snapshot-flag";
|
||||
import { prefetchDashboardSnapshot } from "@/lib/queries/use-dashboard-snapshot";
|
||||
import { prefetchMedicationsList } from "@/lib/queries/prefetch-medications";
|
||||
|
||||
// ── Theme Context ────────────────────────────────────
|
||||
|
||||
@@ -132,6 +133,15 @@ function DashboardSnapshotPreloader() {
|
||||
if (pathname === "/" && isDashboardSnapshotEnabled()) {
|
||||
prefetchDashboardSnapshot(queryClient);
|
||||
}
|
||||
// v1.16.7 — same waterfall cut for the medications page: its list
|
||||
// query (which carries the per-medication `nextDueAt` the due cells
|
||||
// render) used to fire only after the page chunk mounted. Firing it
|
||||
// at route commit rides the data hop in parallel with the chunk
|
||||
// download; the nav links additionally prefetch on hover/touch
|
||||
// intent, so this is the fallback for direct loads + reloads.
|
||||
if (pathname === "/medications") {
|
||||
prefetchMedicationsList(queryClient);
|
||||
}
|
||||
}, [pathname, queryClient]);
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user