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;