From 879833b37264de8015d87014b9dd5b08311c3ad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Tue, 2 Jun 2026 18:39:53 +0200 Subject: [PATCH] fix(insights): stop warming every assessment on an overview visit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opening the Insights overview fired a once-per-session background warm that forced the provider to regenerate the comprehensive briefing and every category and metric assessment at once — a multi-minute burst that contended with foreground requests and made the app feel unresponsive until it finished. The nightly pass keeps the caches warm and the per-metric status GETs revalidate gently on their own, so the warm-on-mount is redundant. Remove it; the manual 'prepare assessments' button remains the explicit on-demand re-warm. --- src/app/insights/page.tsx | 17 +++---- src/components/insights/use-insights-warm.ts | 48 +++----------------- 2 files changed, 13 insertions(+), 52 deletions(-) diff --git a/src/app/insights/page.tsx b/src/app/insights/page.tsx index e3fd38cf6..d4912eb09 100644 --- a/src/app/insights/page.tsx +++ b/src/app/insights/page.tsx @@ -1,6 +1,5 @@ "use client"; -import { useEffect } from "react"; import { useQuery } from "@tanstack/react-query"; import dynamic from "next/dynamic"; import Link from "next/link"; @@ -152,16 +151,12 @@ export default function InsightsPage() { const analyticsQuery = useAnalyticsQuery(); const analytics = analyticsQuery.data as AnalyticsData | undefined; - // v1.8.7.1 — on-demand full assessment warm. The auto-trigger fires once - // per session as soon as the overview confirms the user has data, so the - // status cards (and the iOS client, which reads the same routes) land on - // warm caches without the user waiting. The manual button below re-warms - // everything on demand. - const { warm, isWarming, autoWarmOnce } = useInsightsWarm(); - const hasData = !!data && data.totalMeasurements > 0; - useEffect(() => { - autoWarmOnce(isAuthenticated && hasData); - }, [autoWarmOnce, isAuthenticated, hasData]); + // The "prepare assessments" button below re-warms every assessment on + // demand. There is no warm-on-mount: the nightly cron (04:30) keeps the + // caches warm and the per-metric status GETs revalidate gently on their + // own (stale-while-revalidate), so opening the overview only reads cached + // text — it never fans out a full provider warm on a page visit. + const { warm, isWarming } = useInsightsWarm(); // Empty-state shortcut — only paint once the comprehensive query has // resolved AND reported zero measurements. While it's in-flight we diff --git a/src/components/insights/use-insights-warm.ts b/src/components/insights/use-insights-warm.ts index 53934b35f..5120d5052 100644 --- a/src/components/insights/use-insights-warm.ts +++ b/src/components/insights/use-insights-warm.ts @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useRef } from "react"; +import { useCallback } from "react"; import { useMutation } from "@tanstack/react-query"; import { toast } from "sonner"; @@ -8,7 +8,7 @@ import { queryKeys } from "@/lib/query-keys"; import { useTranslations } from "@/lib/i18n/context"; /** - * v1.8.7.1 — on-demand full assessment warm. + * On-demand full assessment warm. * * POSTs `/api/insights/pregenerate`, which enqueues a forced warm of every * AI assessment (comprehensive insight + the seven specialised status cards @@ -16,18 +16,13 @@ import { useTranslations } from "@/lib/i18n/context"; * the worker and returns immediately. The cards then fill via their existing * stale-while-revalidate GETs — there is nothing to invalidate here. * - * Two entry points share the one mutation: - * - `warm()` — the manual "prepare assessments" button. Shows a toast. - * - `autoWarmOnce(enabled)` — a fire-and-forget background warm fired ONCE - * per browser session (sessionStorage-gated) when the Insights overview - * mounts with data, so a returning user lands on warm caches without - * tapping anything. The auto path is silent (no toast) and tolerates a - * blocked/failed request — the server-side anti-spam bucket and the - * nightly cron are the catch-net. + * This is the explicit "prepare assessments" button only. There is no + * warm-on-mount: the nightly cron (04:30 Europe/Berlin) keeps every user's + * caches warm, and the per-metric status GETs revalidate gently on their + * own — so a page visit reads cached text instead of fanning out a full + * provider warm that would contend with foreground requests. */ -const AUTO_WARM_SESSION_KEY = "healthlog:insights:auto-warm"; - async function postWarm(): Promise { // Same-origin relative-path fetch — exempt from the safe-fetch rule by // construction. A 429 (warm already in progress) is an expected, @@ -47,12 +42,6 @@ export interface UseInsightsWarmResult { warm: () => void; /** True while the warm POST is in flight. */ isWarming: boolean; - /** - * Fire the background warm once per session. Pass `enabled` (e.g. - * authenticated AND the user has data) — the warm only fires on the - * first call where `enabled` is true, and never again this session. - */ - autoWarmOnce: (enabled: boolean) => void; } export function useInsightsWarm(): UseInsightsWarmResult { @@ -71,31 +60,8 @@ export function useInsightsWarm(): UseInsightsWarmResult { }); }, [mutate, t]); - // The auto-trigger must fire at most once per session even across the - // remounts a route navigation causes. A ref guards within the mount; - // sessionStorage guards across mounts / navigations. - const firedThisMount = useRef(false); - const autoWarmOnce = useCallback( - (enabled: boolean) => { - if (!enabled || firedThisMount.current) return; - firedThisMount.current = true; - try { - if (window.sessionStorage.getItem(AUTO_WARM_SESSION_KEY)) return; - window.sessionStorage.setItem(AUTO_WARM_SESSION_KEY, String(Date.now())); - } catch { - // sessionStorage can throw under strict-privacy modes — fall - // through and fire the warm anyway; the server bucket de-dupes. - } - // Silent background warm — no toast, errors swallowed by the - // mutation's default (no onError handler surfaces it). - mutate(); - }, - [mutate], - ); - return { warm, isWarming: mutation.isPending, - autoWarmOnce, }; }