From b00450aec8e6fc89bedd7590f3bd0645d8f7aa67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 18 Jul 2026 03:48:51 +0200 Subject: [PATCH] fix(pwa): fix pull-to-refresh's at-top check and extend coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The authenticated shell's actual scroll container is
(AuthShell sets it overflow-y-auto inside a height-locked layout); document.scrollingElement never moves there, so the gesture's "only arm at the top of the page" check was permanently true regardless of how far a user had scrolled a long list. Point it at the real scroll container instead, with the old document-root check kept as a fallback for the few body-scrolled surfaces outside the shell. Extend pull-to-refresh from 4 pages (labs/checkups/mood/measurements) to the dashboard, /insights, /medications, and the documents vault — the sharpest PWA-resume-staleness surfaces, per the audit: a background-refreshed dose or a cross-device write otherwise sits stale until a full remount or the staleTime window lapses. --- src/app/insights/page.tsx | 21 ++++++++++++++++++-- src/app/medications/page.tsx | 22 +++++++++++++++++++-- src/app/page-client.tsx | 22 +++++++++++++++++++-- src/components/documents/documents-view.tsx | 15 ++++++++++++++ src/hooks/use-pull-to-refresh.ts | 18 +++++++++++++++-- 5 files changed, 90 insertions(+), 8 deletions(-) diff --git a/src/app/insights/page.tsx b/src/app/insights/page.tsx index 4d8ea36cd..07bbfb523 100644 --- a/src/app/insights/page.tsx +++ b/src/app/insights/page.tsx @@ -1,7 +1,7 @@ "use client"; -import { Fragment, type ReactNode } from "react"; -import { useQuery } from "@tanstack/react-query"; +import { Fragment, useCallback, type ReactNode } from "react"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import dynamic from "next/dynamic"; import Link from "next/link"; import { SlidersHorizontal, TrendingUp } from "lucide-react"; @@ -11,6 +11,8 @@ import { queryKeys } from "@/lib/query-keys"; import { apiGet } from "@/lib/api/api-fetch"; import { useFeatureFlags } from "@/hooks/use-feature-flags"; import { useInsightsLayoutQuery } from "@/hooks/use-insights-layout"; +import { usePullToRefresh } from "@/hooks/use-pull-to-refresh"; +import { PullToRefreshIndicator } from "@/components/ui/pull-to-refresh-indicator"; import { orderedVisibleSectionIds, type InsightsSectionId, @@ -263,6 +265,20 @@ export default function InsightsPage() { const { isAuthenticated, user } = useAuth(); const mounted = useMounted(); const { t } = useTranslations(); + const queryClient = useQueryClient(); + + // v1.30.1 M12 — the overview fans out into ~6 independent queries + // (comprehensive / advisor / analytics / snapshot / derived batch / + // layout) mounted by different hooks below, so there's no single + // `refetch()` that covers the page. Refetching every currently-mounted + // ("active") query is the same blanket approach the audit's fallback + // suggests for PWA-resume staleness, and it's the right shape here + // regardless of resume vs. an explicit pull. + const refreshInsights = useCallback( + () => queryClient.refetchQueries({ type: "active" }), + [queryClient], + ); + const pull = usePullToRefresh({ onRefresh: refreshInsights }); // v1.4.33 IW9 — scroll-to-top on route mount centralised in the // shared `useScrollResetOnRoute()` hook. The mother page + the @@ -531,6 +547,7 @@ export default function InsightsPage() { // zero-gap). Each section owns its OWN `SectionHeading` + `space-y-3` to its // card, so the rhythm is even regardless of which sections are present.
+ invalidateKeys(queryClient, medicationDependentKeys), + [queryClient], + ); + const pull = usePullToRefresh({ onRefresh: refreshMedications }); + // v1.29.x — once the highlighted card is in the DOM, scroll it into view // and clear the highlight state after a few seconds so the ring is a // one-time "you tapped here" cue, not a permanent tint (mirrors the @@ -363,6 +380,7 @@ export default function MedicationsPage() { return (
+

(null); + + // v1.30.1 M12 — pull-to-refresh parity for the dashboard, named as one + // of the sharpest PWA-resume-staleness surfaces in the audit (it + // already opts back into `refetchOnWindowFocus`, but a PWA resumed + // from the home screen/app-switcher, not a browser tab focus event, + // is the more common mobile path and that never fires a focus event). + // The dashboard fans out into a dozen-plus independent per-tile/chart + // queries, so — like `/insights` — refetching every currently-active + // query is the only tractable "refresh everything visible" shape. + const refreshDashboard = useCallback( + () => queryClient.refetchQueries({ type: "active" }), + [queryClient], + ); + const pull = usePullToRefresh({ onRefresh: refreshDashboard }); // v1.4.29 M5 — the three inline dashboard queries default to a // 0-ms `staleTime`, so a tab-focus-and-return triggered a refetch // storm. None of these are real-time data; minute-scale staleness @@ -785,6 +802,7 @@ export default function DashboardPageClient() { return (
+ {/* S2 — the Today hero, promoted above the tile strip (MARC sign-off diff --git a/src/components/documents/documents-view.tsx b/src/components/documents/documents-view.tsx index d9d61b9ef..a1227eaf8 100644 --- a/src/components/documents/documents-view.tsx +++ b/src/components/documents/documents-view.tsx @@ -38,6 +38,8 @@ import { PageHeader } from "@/components/ui/page-header"; import { QueryErrorCard } from "@/components/ui/query-error-card"; import { Skeleton } from "@/components/ui/skeleton"; import { useAuth } from "@/hooks/use-auth"; +import { usePullToRefresh } from "@/hooks/use-pull-to-refresh"; +import { PullToRefreshIndicator } from "@/components/ui/pull-to-refresh-indicator"; import { apiGet, apiPost } from "@/lib/api/api-fetch"; import { useTranslations } from "@/lib/i18n/context"; import { invalidateKeys, queryKeys } from "@/lib/query-keys"; @@ -233,6 +235,18 @@ export function DocumentsView() { [list.data], ); + // v1.30.1 M12 — pull-to-refresh parity with labs/checkups/mood/ + // measurements/medications. The vault is one of the sharpest PWA-resume- + // staleness surfaces named in the audit: a document indexed server-side + // while the tab was backgrounded (auto-index / AI summary) otherwise + // stays invisible until the bounded processing poll above happens to + // catch it or the user navigates away and back. + const refreshDocuments = useCallback( + () => Promise.all([list.refetch(), usage.refetch()]), + [list, usage], + ); + const pull = usePullToRefresh({ onRefresh: refreshDocuments }); + const upload = useDocumentUpload( usage.data ? { maxFileBytes: usage.data.maxFileBytes } : undefined, ); @@ -595,6 +609,7 @@ export function DocumentsView() { return (
+ - (document.scrollingElement?.scrollTop ?? window.scrollY) <= 0; + // v1.30.1 M12 — the authenticated shell's actual scroll container is + // `
` (`AuthShell` sets it `overflow-y-auto` + // inside a height-locked `h-dvh` layout); `document.scrollingElement` + // never moves there; it stays permanently at 0. Checking the document + // root meant "at top" was always true regardless of how far the user + // had actually scrolled a long list — a downward drag mid-scroll could + // arm the pull gesture instead of just scrolling the list. Falls back + // to the document root for the few body-scrolled surfaces (public + // pages) that render outside the shell's `#main-content`. + const atTop = () => { + const main = document.getElementById("main-content"); + const scrollTop = main + ? main.scrollTop + : (document.scrollingElement?.scrollTop ?? window.scrollY); + return scrollTop <= 0; + }; const reset = () => { startY = null;