mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
fix(daily-loop): invalidate the Today digest on measurement/mood/water writes
Manual measurement, mood, and water writes never carried queryKeys.dailyDigest() in their invalidation bundles — only medication intake did (v1.29.1). The digest query also runs with refetchOnMount/WindowFocus off, so a write from the values list, the mood list, or the hydration quick-add sheet left the Today hero's score and rail items stale until a hard reload. Adds dailyDigest() to measurementDependentKeys and moodDependentKeys, and adds a shared refetchInactiveDailyReads() helper (mirrors the existing medication seam) that forces the dashboard snapshot and digest to refetch even while unmounted. Every measurement/mood/water write call site now runs it alongside its existing *DependentKeys bundle.
This commit is contained in:
1 parent
bf1c052797
commit
844b24c7ba
7 files changed
+125
-4
No files matched your search
@@ -10,7 +10,7 @@ import { Label } from "@/components/ui/label";
|
||||
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { apiPost } from "@/lib/api/api-fetch";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { queryKeys, refetchInactiveDailyReads } from "@/lib/query-keys";
|
||||
|
||||
/**
|
||||
* v1.29 — manual water quick-add sheet, opened from the hydration card.
|
||||
@@ -45,11 +45,21 @@ export function WaterQuickAddSheet({
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [editAmount, setEditAmount] = useState(() => String(todayTotalMl));
|
||||
|
||||
// v1.29.x — the Today digest joins the invalidation. The write route
|
||||
// already hard-evicts the server snapshot bucket
|
||||
// (`invalidateUserDashboardSnapshot`), but the client digest/snapshot
|
||||
// queries are typically unmounted while this sheet is open (the user is
|
||||
// on `/insights/nutrients`, not the dashboard), so a default ("active")
|
||||
// invalidation would mark them stale without refetching them —
|
||||
// `refetchInactiveDailyReads` forces the inactive refetch, mirroring the
|
||||
// measurement / mood / medication fix.
|
||||
const invalidate = () => {
|
||||
void queryClient.invalidateQueries({ queryKey: ["nutrients"] });
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.dashboardSnapshot(),
|
||||
});
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.dailyDigest() });
|
||||
void refetchInactiveDailyReads(queryClient);
|
||||
};
|
||||
|
||||
const addMutation = useMutation({
|
||||
|
||||
@@ -24,7 +24,11 @@ import {
|
||||
import { Loader2, MoreHorizontal, Plus, RotateCcw } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { invalidateKeys, measurementDependentKeys } from "@/lib/query-keys";
|
||||
import {
|
||||
invalidateKeys,
|
||||
measurementDependentKeys,
|
||||
refetchInactiveDailyReads,
|
||||
} from "@/lib/query-keys";
|
||||
import { ApiError, apiPost } from "@/lib/api/api-fetch";
|
||||
import { MEASUREMENT_NOTES_MAX_LENGTH } from "@/lib/validations/measurement";
|
||||
|
||||
@@ -326,6 +330,7 @@ export function MeasurementForm({
|
||||
setPulse("");
|
||||
setNotes("");
|
||||
await invalidateKeys(queryClient, measurementDependentKeys);
|
||||
await refetchInactiveDailyReads(queryClient);
|
||||
toast.success(t("common.saved"));
|
||||
onSuccess?.();
|
||||
} catch (err) {
|
||||
|
||||
@@ -66,6 +66,7 @@ import {
|
||||
invalidateKeys,
|
||||
measurementDependentKeys,
|
||||
queryKeys,
|
||||
refetchInactiveDailyReads,
|
||||
} from "@/lib/query-keys";
|
||||
import {
|
||||
MEASUREMENT_NOTES_MAX_LENGTH,
|
||||
@@ -544,6 +545,7 @@ export function MeasurementList({
|
||||
try {
|
||||
await apiPost("/api/measurements/restore", { ids });
|
||||
await invalidateKeys(queryClient, measurementDependentKeys);
|
||||
await refetchInactiveDailyReads(queryClient);
|
||||
toast.success(t("measurements.restoredToast"));
|
||||
} catch {
|
||||
toast.error(t("measurements.restoreError"));
|
||||
@@ -558,6 +560,7 @@ export function MeasurementList({
|
||||
},
|
||||
onSuccess: (_data, id) => {
|
||||
void invalidateKeys(queryClient, measurementDependentKeys);
|
||||
void refetchInactiveDailyReads(queryClient);
|
||||
toast.success(t("measurements.deletedToast"), {
|
||||
action: {
|
||||
label: t("common.undo"),
|
||||
@@ -587,6 +590,7 @@ export function MeasurementList({
|
||||
},
|
||||
onSuccess: async (deleted, ids) => {
|
||||
await invalidateKeys(queryClient, measurementDependentKeys);
|
||||
await refetchInactiveDailyReads(queryClient);
|
||||
clearSelection();
|
||||
toast.success(
|
||||
t("measurements.bulkDeleteSuccess", { count: String(deleted) }),
|
||||
@@ -649,6 +653,7 @@ export function MeasurementList({
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await invalidateKeys(queryClient, measurementDependentKeys);
|
||||
await refetchInactiveDailyReads(queryClient);
|
||||
setEditing(null);
|
||||
setEditError(null);
|
||||
},
|
||||
|
||||
@@ -37,7 +37,11 @@ import {
|
||||
import { toast } from "sonner";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { useRovingRadioGroup } from "@/hooks/use-roving-radio-group";
|
||||
import { invalidateKeys, moodDependentKeys } from "@/lib/query-keys";
|
||||
import {
|
||||
invalidateKeys,
|
||||
moodDependentKeys,
|
||||
refetchInactiveDailyReads,
|
||||
} from "@/lib/query-keys";
|
||||
import { ApiError, apiPost } from "@/lib/api/api-fetch";
|
||||
import { SheetSection, SheetSectionCount } from "@/components/ui/sheet-section";
|
||||
import { MoodTagPicker, type RatedFactor } from "./mood-tag-picker";
|
||||
@@ -216,6 +220,7 @@ export function MoodForm({ onSuccess, onCancel, footerSlot }: MoodFormProps) {
|
||||
resetForm();
|
||||
setMoreTagsOpen(false);
|
||||
await invalidateKeys(queryClient, moodDependentKeys);
|
||||
await refetchInactiveDailyReads(queryClient);
|
||||
toast.success(t("common.saved"));
|
||||
onSuccess?.();
|
||||
} catch (err) {
|
||||
|
||||
@@ -62,7 +62,12 @@ import { toast } from "sonner";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import { MOOD_LABEL_KEYS, MOOD_SCORE_BY_ENUM } from "@/lib/mood/labels";
|
||||
import { invalidateKeys, moodDependentKeys, queryKeys } from "@/lib/query-keys";
|
||||
import {
|
||||
invalidateKeys,
|
||||
moodDependentKeys,
|
||||
queryKeys,
|
||||
refetchInactiveDailyReads,
|
||||
} from "@/lib/query-keys";
|
||||
import { moodSourceEnum } from "@/lib/validations/moodlog";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
@@ -268,6 +273,7 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
|
||||
try {
|
||||
await apiPost("/api/mood-entries/restore", { ids });
|
||||
await invalidateKeys(queryClient, moodDependentKeys);
|
||||
await refetchInactiveDailyReads(queryClient);
|
||||
toast.success(t("mood.restoredToast"));
|
||||
} catch {
|
||||
toast.error(t("mood.restoreError"));
|
||||
@@ -282,6 +288,7 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
|
||||
},
|
||||
onSuccess: (_data, id) => {
|
||||
void invalidateKeys(queryClient, moodDependentKeys);
|
||||
void refetchInactiveDailyReads(queryClient);
|
||||
toast.success(t("mood.deletedToast"), {
|
||||
action: {
|
||||
label: t("common.undo"),
|
||||
@@ -309,6 +316,7 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
|
||||
},
|
||||
onSuccess: async (deleted, ids) => {
|
||||
await invalidateKeys(queryClient, moodDependentKeys);
|
||||
await refetchInactiveDailyReads(queryClient);
|
||||
clearSelection();
|
||||
toast.success(t("mood.bulkDeleteSuccess", { count: String(deleted) }), {
|
||||
action: {
|
||||
@@ -369,6 +377,7 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await invalidateKeys(queryClient, moodDependentKeys);
|
||||
await refetchInactiveDailyReads(queryClient);
|
||||
setEditing(null);
|
||||
setEditError(null);
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
moodDependentKeys,
|
||||
medicationDependentKeys,
|
||||
invalidateKeys,
|
||||
refetchInactiveDailyReads,
|
||||
} from "../query-keys";
|
||||
import type { QueryClient, QueryKey } from "@tanstack/react-query";
|
||||
|
||||
@@ -262,6 +263,15 @@ describe("dependent-key bundles", () => {
|
||||
expect(keyStrings).toContain(JSON.stringify(["dashboard", "snapshot"]));
|
||||
});
|
||||
|
||||
// v1.29.x — a manual measurement write never invalidated the Today
|
||||
// digest, so its score / rail items lingered stale after a save from
|
||||
// the values list or the dashboard quick-entry sheet. Mirror the
|
||||
// v1.29.1 medication fix.
|
||||
it("measurementDependentKeys bundles the daily digest", () => {
|
||||
const keyStrings = measurementDependentKeys.map((k) => JSON.stringify(k));
|
||||
expect(keyStrings).toContain(JSON.stringify(["daily", "digest"]));
|
||||
});
|
||||
|
||||
it("moodDependentKeys bundle covers mood + analytics + targets", () => {
|
||||
const keyStrings = moodDependentKeys.map((k) => JSON.stringify(k));
|
||||
expect(keyStrings).toContain(JSON.stringify(["mood-entries"]));
|
||||
@@ -279,6 +289,13 @@ describe("dependent-key bundles", () => {
|
||||
expect(keyStrings).toContain(JSON.stringify(["dashboard", "snapshot"]));
|
||||
});
|
||||
|
||||
// v1.29.x — mirrors the measurement fix: a manual mood write never
|
||||
// invalidated the Today digest.
|
||||
it("moodDependentKeys bundles the daily digest", () => {
|
||||
const keyStrings = moodDependentKeys.map((k) => JSON.stringify(k));
|
||||
expect(keyStrings).toContain(JSON.stringify(["daily", "digest"]));
|
||||
});
|
||||
|
||||
it("medicationDependentKeys bundle covers medications + analytics + achievements", () => {
|
||||
const keyStrings = medicationDependentKeys.map((k) => JSON.stringify(k));
|
||||
expect(keyStrings).toContain(JSON.stringify(["medications"]));
|
||||
@@ -467,3 +484,28 @@ describe("invalidateKeys", () => {
|
||||
expect(results[2].status).toBe("fulfilled");
|
||||
});
|
||||
});
|
||||
|
||||
// v1.29.x — the shared helper behind the measurement / mood / water fix.
|
||||
// `invalidateKeys` (default `refetchType: "active"`) only refetches MOUNTED
|
||||
// queries, so the dashboard hero and Today digest — both typically unmounted
|
||||
// while the user is on the measurement/mood/nutrients surface — are marked
|
||||
// stale but never refetched. Mirrors the v1.29.1 medication-intake fix
|
||||
// (`invalidateMedicationReads`); this is the shared version every other
|
||||
// write seam now calls alongside its own `*DependentKeys` bundle.
|
||||
describe("refetchInactiveDailyReads", () => {
|
||||
it("forces an inactive refetch of both the dashboard snapshot and the Today digest", async () => {
|
||||
const invalidate = vi.fn().mockResolvedValue(undefined);
|
||||
const client = { invalidateQueries: invalidate } as unknown as QueryClient;
|
||||
|
||||
await refetchInactiveDailyReads(client);
|
||||
|
||||
expect(invalidate).toHaveBeenCalledWith({
|
||||
queryKey: queryKeys.dashboardSnapshot(),
|
||||
refetchType: "inactive",
|
||||
});
|
||||
expect(invalidate).toHaveBeenCalledWith({
|
||||
queryKey: queryKeys.dailyDigest(),
|
||||
refetchType: "inactive",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -92,6 +92,14 @@ export const measurementDependentKeys = [
|
||||
queryKeys.insightsTargets(),
|
||||
queryKeys.gamificationAchievements(),
|
||||
queryKeys.dashboardSnapshot(),
|
||||
// v1.29.x — the Today digest reads the same snapshot ingredients a
|
||||
// manual measurement write already hard-evicts server-side; joins the
|
||||
// bundle so the digest's score / rail items refresh in lockstep with
|
||||
// the tile strip. Call sites also force an inactive refetch (see
|
||||
// `refetchInactiveDailyReads` below) since a default invalidation only
|
||||
// refetches MOUNTED queries and the digest is typically unmounted
|
||||
// while the user is on the measurement surface.
|
||||
queryKeys.dailyDigest(),
|
||||
["chart-data"] as const,
|
||||
// v1.8.5 — re-run the diversity-nudge clustering when readings change.
|
||||
["measurement-diversity"] as const,
|
||||
@@ -127,6 +135,11 @@ export const moodDependentKeys = [
|
||||
queryKeys.insightsTargets(),
|
||||
queryKeys.gamificationAchievements(),
|
||||
queryKeys.dashboardSnapshot(),
|
||||
// v1.29.x — mirrors the measurement fix above: the Today digest reads
|
||||
// the same snapshot mood block a mood write already hard-evicts
|
||||
// server-side. Call sites force an inactive refetch too (see
|
||||
// `refetchInactiveDailyReads`).
|
||||
queryKeys.dailyDigest(),
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -208,3 +221,35 @@ export function invalidateKeys(
|
||||
keys.map((queryKey) => queryClient.invalidateQueries({ queryKey })),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force the dashboard snapshot AND the Today digest to refetch even while
|
||||
* unmounted. `invalidateKeys` (and every `*DependentKeys` bundle above) marks
|
||||
* a query stale with the default `refetchType: "active"`, which only
|
||||
* refetches MOUNTED queries — so a write made from the measurement / mood /
|
||||
* medication surface leaves the dashboard hero and the Today digest (both
|
||||
* typically unmounted while the user is elsewhere) stale until a manual
|
||||
* remount re-fetches under `refetchOnMount: false`.
|
||||
*
|
||||
* Originates from the v1.16.11 dashboard-snapshot fix and the v1.29.1
|
||||
* medication-intake fix (`invalidateMedicationReads` in
|
||||
* `components/medications/use-medication-intake.ts`, which predates this
|
||||
* shared helper and keeps its own inline copy). Call this alongside
|
||||
* `invalidateKeys(queryClient, xDependentKeys)` from any write whose server
|
||||
* route already hard-evicts the snapshot bucket — the forced refetch then
|
||||
* returns post-write data immediately, no server change needed.
|
||||
*/
|
||||
export async function refetchInactiveDailyReads(
|
||||
queryClient: QueryClient,
|
||||
): Promise<void> {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.dashboardSnapshot(),
|
||||
refetchType: "inactive",
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.dailyDigest(),
|
||||
refetchType: "inactive",
|
||||
}),
|
||||
]);
|
||||
}
|
||||
Reference in new issue
Block a user