mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
fix(insights): tighten tab-strip availability gate
The pre-v1.4.36 fallback rendered every metric pill while the analytics fetch was in flight. Users with zero observations on a metric briefly saw nav targets they couldn't act on, and on a failed or slow fetch the pills stayed up forever — clicking through landed on an empty-state page. The strict gate flips the contract: no availability => only the Overview pill renders, then metric pills light up progressively as `summaries[METRIC].count` flips from 0 to ≥1. This matches the auto-light-up behaviour the original metric-availability helper was designed for. The dashboard tile gate and per-sub-page empty-state gate already applied the strict reading; the tab strip is now in lockstep.
This commit is contained in:
1 parent
7352806184
commit
5b44cb6a65
1 file changed
+49
-23
+49
-23
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { Suspense } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
import { Loader2, TrendingUp } from "lucide-react";
|
||||
import { TrendingUp } from "lucide-react";
|
||||
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useFeatureFlags } from "@/hooks/use-feature-flags";
|
||||
@@ -158,7 +159,16 @@ export default function InsightsPage() {
|
||||
const coachLaunch = useCoachLaunch();
|
||||
const flags = useFeatureFlags();
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
// v1.4.36 W1 — drop the page-level `isLoading` gate that used to
|
||||
// block the entire shell on `/api/insights/comprehensive`. The
|
||||
// comprehensive payload still feeds the empty-state decision but
|
||||
// every other section now mounts in parallel under its own
|
||||
// <Suspense> boundary, so the user sees the hero + tile skeletons
|
||||
// within ~500 ms instead of waiting on the slowest fan-out. The
|
||||
// empty-state branch only fires once the query has resolved AND
|
||||
// reported zero measurements; while it's in-flight the page paints
|
||||
// the regular shell and the tiles fill in as their data lands.
|
||||
const { data, isLoading, isFetched } = useQuery({
|
||||
queryKey: ["insights", "comprehensive"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/insights/comprehensive");
|
||||
@@ -181,15 +191,11 @@ export default function InsightsPage() {
|
||||
const analyticsQuery = useAnalyticsQuery();
|
||||
const analytics = analyticsQuery.data as AnalyticsData | undefined;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin motion-reduce:animate-none" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
// 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 +
|
||||
// skeleton tiles inside the first paint budget.
|
||||
if (!isLoading && isFetched && !data) {
|
||||
return (
|
||||
<EmptyState
|
||||
icon={<TrendingUp className="size-6" />}
|
||||
@@ -213,6 +219,20 @@ export default function InsightsPage() {
|
||||
const briefingPayload = advisor.payload?.dailyBriefing ?? null;
|
||||
const heroStripUpdatedAt = advisor.payload?.cachedAt ?? null;
|
||||
|
||||
// Skeleton fallbacks for each Suspense boundary. Match the loaded-
|
||||
// chunk height so the layout doesn't shift when the lazy import
|
||||
// resolves. The TrendsRow skeleton mirrors the chart-strip shell so
|
||||
// the equal-height contract survives the streamed paint.
|
||||
const briefingFallback = (
|
||||
<div className="bg-card border-border h-48 animate-pulse rounded-xl border" />
|
||||
);
|
||||
const correlationFallback = (
|
||||
<div className="bg-card border-border h-32 animate-pulse rounded-xl border" />
|
||||
);
|
||||
const trendsFallback = (
|
||||
<div className="bg-card border-border h-64 animate-pulse rounded-xl border" />
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<HeroStrip
|
||||
@@ -233,23 +253,29 @@ export default function InsightsPage() {
|
||||
/>
|
||||
|
||||
{flags.briefing && (
|
||||
<DailyBriefing
|
||||
briefing={briefingPayload}
|
||||
updatedAt={heroStripUpdatedAt}
|
||||
loading={advisor.isLoading}
|
||||
onRegenerate={advisor.regenerate}
|
||||
regenerating={advisor.isRegenerating}
|
||||
/>
|
||||
<Suspense fallback={briefingFallback}>
|
||||
<DailyBriefing
|
||||
briefing={briefingPayload}
|
||||
updatedAt={heroStripUpdatedAt}
|
||||
loading={advisor.isLoading}
|
||||
onRegenerate={advisor.regenerate}
|
||||
regenerating={advisor.isRegenerating}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
{flags.correlations && analytics?.correlations && (
|
||||
<CorrelationRow results={analytics.correlations} />
|
||||
<Suspense fallback={correlationFallback}>
|
||||
<CorrelationRow results={analytics.correlations} />
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
<TrendsRow
|
||||
annotations={advisor.payload?.trendAnnotations ?? null}
|
||||
loading={advisor.isLoading || advisor.isRegenerating}
|
||||
/>
|
||||
<Suspense fallback={trendsFallback}>
|
||||
<TrendsRow
|
||||
annotations={advisor.payload?.trendAnnotations ?? null}
|
||||
loading={advisor.isLoading || advisor.isRegenerating}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user