mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
feat(insights): warm assessments from the overview page
Wire the on-demand full assessment warm into the Insights overview. A new
useInsightsWarm hook POSTs /api/insights/pregenerate; the cards then fill
through their existing stale-while-revalidate reads with nothing to
invalidate locally.
Two entry points share the one mutation. A background warm fires once per
browser session (sessionStorage-gated) as soon as the overview confirms
the user has data, so a returning user lands on warm caches without
acting; it is silent and tolerates the 429 the server returns while a
warm is already in flight. A manual button ('Prepare assessments') runs
the same warm and shows a toast.
Add the insightsPregenerate mutationKey to the factory and the label +
toast strings across all six locales.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 1273a6253dc0d00c7c7ce774c5fbe352664de2e9)
This commit is contained in:
1 parent
bf5052bb01
commit
2472579059
9 files changed
+148
-1
No files matched your search
@@ -1501,6 +1501,8 @@
|
||||
"heroRegenerating": "Wird generiert",
|
||||
"regenerateAnalysis": "Analyse neu starten",
|
||||
"regenerateSuccess": "Analyse wurde neu erstellt",
|
||||
"warmAssessments": "Auswertungen vorbereiten",
|
||||
"warmStarted": "Auswertungen werden im Hintergrund erstellt",
|
||||
"heroFallbackSubtitle": "Täglicher Blick auf deine Trends — direkt aus den Zahlen, die du protokollierst.",
|
||||
"heroGreetingMorning": "Guten Morgen",
|
||||
"heroGreetingAfternoon": "Guten Tag",
|
||||
|
||||
@@ -1501,6 +1501,8 @@
|
||||
"heroRegenerating": "Regenerating",
|
||||
"regenerateAnalysis": "Re-run analysis",
|
||||
"regenerateSuccess": "Analysis refreshed",
|
||||
"warmAssessments": "Prepare assessments",
|
||||
"warmStarted": "Assessments are being prepared in the background",
|
||||
"heroFallbackSubtitle": "A daily read of your trends, drawn straight from the numbers you've logged.",
|
||||
"heroGreetingMorning": "Good morning",
|
||||
"heroGreetingAfternoon": "Good afternoon",
|
||||
|
||||
@@ -1501,6 +1501,8 @@
|
||||
"heroRegenerating": "Generando",
|
||||
"regenerateAnalysis": "Volver a iniciar el análisis",
|
||||
"regenerateSuccess": "Análisis regenerado",
|
||||
"warmAssessments": "Preparar evaluaciones",
|
||||
"warmStarted": "Las evaluaciones se están preparando en segundo plano",
|
||||
"heroFallbackSubtitle": "Una mirada diaria a tus tendencias — directamente de los datos que registras.",
|
||||
"heroGreetingMorning": "Buenos días",
|
||||
"heroGreetingAfternoon": "Buenas tardes",
|
||||
|
||||
@@ -1501,6 +1501,8 @@
|
||||
"heroRegenerating": "Génération en cours",
|
||||
"regenerateAnalysis": "Relancer l’analyse",
|
||||
"regenerateSuccess": "Analyse regénérée",
|
||||
"warmAssessments": "Préparer les évaluations",
|
||||
"warmStarted": "Les évaluations sont en cours de préparation en arrière-plan",
|
||||
"heroFallbackSubtitle": "Un coup d’œil quotidien sur tes tendances — directement issu des chiffres que tu saisis.",
|
||||
"heroGreetingMorning": "Bonjour",
|
||||
"heroGreetingAfternoon": "Bonjour",
|
||||
|
||||
@@ -1501,6 +1501,8 @@
|
||||
"heroRegenerating": "Generazione in corso",
|
||||
"regenerateAnalysis": "Riavvia l’analisi",
|
||||
"regenerateSuccess": "Analisi rigenerata",
|
||||
"warmAssessments": "Prepara le valutazioni",
|
||||
"warmStarted": "Le valutazioni vengono preparate in background",
|
||||
"heroFallbackSubtitle": "Uno sguardo quotidiano ai tuoi trend — direttamente dai numeri che registri.",
|
||||
"heroGreetingMorning": "Buongiorno",
|
||||
"heroGreetingAfternoon": "Buon pomeriggio",
|
||||
|
||||
@@ -1501,6 +1501,8 @@
|
||||
"heroRegenerating": "Generowanie",
|
||||
"regenerateAnalysis": "Uruchom analizę ponownie",
|
||||
"regenerateSuccess": "Analiza wygenerowana ponownie",
|
||||
"warmAssessments": "Przygotuj analizy",
|
||||
"warmStarted": "Analizy są przygotowywane w tle",
|
||||
"heroFallbackSubtitle": "Codzienne spojrzenie na twoje trendy — prosto z liczb, które rejestrujesz.",
|
||||
"heroGreetingMorning": "Dzień dobry",
|
||||
"heroGreetingAfternoon": "Dzień dobry",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
import { TrendingUp } from "lucide-react";
|
||||
import { TrendingUp, Sparkles } from "lucide-react";
|
||||
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
@@ -14,6 +15,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { HeroStrip } from "@/components/insights/hero-strip";
|
||||
import { useInsightsAdvisorQuery } from "@/components/insights/use-insights-advisor";
|
||||
import { useInsightsWarm } from "@/components/insights/use-insights-warm";
|
||||
import { useCoachLaunch } from "@/lib/insights/coach-launch-context";
|
||||
import { useAnalyticsQuery } from "@/lib/queries/use-analytics-query";
|
||||
// v1.4.41 W-ORG — shared shape lives in `src/types/analytics.ts` as
|
||||
@@ -150,6 +152,17 @@ 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]);
|
||||
|
||||
// Empty-state shortcut — only paint once the comprehensive query has
|
||||
// resolved AND reported zero measurements. While it's in-flight we
|
||||
// fall through to the streamed shell so the user gets the hero +
|
||||
@@ -210,6 +223,18 @@ export default function InsightsPage() {
|
||||
healthScore={analytics?.healthScore ?? undefined}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={warm}
|
||||
disabled={isWarming}
|
||||
>
|
||||
<Sparkles className="size-4" />
|
||||
{t("insights.warmAssessments")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{flags.briefing && (
|
||||
<DailyBriefing
|
||||
briefing={briefingPayload}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useRef } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
/**
|
||||
* v1.8.7.1 — 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
|
||||
* + every data-bearing generic metric assessment) for the calling user on
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const AUTO_WARM_SESSION_KEY = "healthlog:insights:auto-warm";
|
||||
|
||||
async function postWarm(): Promise<void> {
|
||||
// Same-origin relative-path fetch — exempt from the safe-fetch rule by
|
||||
// construction. A 429 (warm already in progress) is an expected,
|
||||
// harmless outcome, not an error.
|
||||
const res = await fetch("/api/insights/pregenerate", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: "{}",
|
||||
});
|
||||
if (!res.ok && res.status !== 429) {
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
export interface UseInsightsWarmResult {
|
||||
/** Manual trigger — POSTs the warm and shows a toast. */
|
||||
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 {
|
||||
const { t } = useTranslations();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationKey: queryKeys.insightsPregenerate(),
|
||||
mutationFn: postWarm,
|
||||
});
|
||||
|
||||
const { mutate } = mutation;
|
||||
|
||||
const warm = useCallback(() => {
|
||||
mutate(undefined, {
|
||||
onSuccess: () => toast.success(t("insights.warmStarted")),
|
||||
});
|
||||
}, [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,
|
||||
};
|
||||
}
|
||||
@@ -113,6 +113,15 @@ export const queryKeys = {
|
||||
*/
|
||||
insightsMetricStatus: (metric: string, locale: string) =>
|
||||
["insights", "metric-status", metric, locale] as const,
|
||||
/**
|
||||
* v1.8.7.1 — mutationKey for the on-demand full-warm POST
|
||||
* (`/api/insights/pregenerate`). Lives in the factory so the bare-array
|
||||
* ESLint rule stays satisfied; the warm enqueues every assessment
|
||||
* generation on the worker, so there is no read cache to invalidate
|
||||
* here — the existing status queries pick up the warmed text via their
|
||||
* own stale-while-revalidate.
|
||||
*/
|
||||
insightsPregenerate: () => ["insights", "pregenerate"] as const,
|
||||
|
||||
medications: () => ["medications"] as const,
|
||||
medicationDetail: (id: string) => ["medications", id] as const,
|
||||
|
||||
Reference in new issue
Block a user