mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
Merge wave-qol-mechanical: route stray query keys through the factory
Fix the target-edit-sheet invalidation that used a bare ["insights"] array not matching the factory prefix the reads nest under (a silent no-op), and migrate 12 further bare queryKey call sites onto the centralised factory.
This commit is contained in:
commit
572ff3502e
16 files changed
+80
-24
No files matched your search
@@ -281,7 +281,7 @@ export function MedicationIntakeQuickAdd({
|
||||
// Fan-out to every inline compliance chart key (one per medication)
|
||||
// so the detail-page tile refreshes when the user re-enters it.
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["compliance-chart-inline"],
|
||||
queryKey: queryKeys.complianceChartInline(),
|
||||
});
|
||||
|
||||
toast.success(t("common.saved"));
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
CoachSuggestion,
|
||||
} from "@/lib/ai/coach/types";
|
||||
import { apiDelete, apiFetchRaw, apiGet } from "@/lib/api/api-fetch";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
/**
|
||||
* v1.4.20 phase B2b — TanStack Query + SSE client for the AI Coach
|
||||
@@ -32,8 +33,8 @@ import { apiDelete, apiFetchRaw, apiGet } from "@/lib/api/api-fetch";
|
||||
*/
|
||||
|
||||
const QUERY_KEYS = {
|
||||
list: () => ["coachConversations"] as const,
|
||||
one: (id: string) => ["coachConversation", id] as const,
|
||||
list: () => queryKeys.coachConversations(),
|
||||
one: (id: string) => queryKeys.coachConversation(id),
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -74,7 +75,7 @@ export function useCoachConversation(id: string | null) {
|
||||
// never fires when `id` is null. Key directly on `id` (TanStack
|
||||
// Query accepts `null` in the array) — the previous "null"
|
||||
// placeholder branch was structurally equivalent dead code.
|
||||
queryKey: ["coachConversation", id] as const,
|
||||
queryKey: queryKeys.coachConversation(id),
|
||||
queryFn: async (): Promise<CoachConversationDetailDTO> => {
|
||||
if (!id) throw new Error("missing id");
|
||||
return apiGet<CoachConversationDetailDTO>(`/api/insights/chat/${id}`, {
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||
import { apiGet } from "@/lib/api/api-fetch";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import type { MeasurementType } from "@/generated/prisma/client";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -75,7 +76,7 @@ export function PersonalRecordBadge({
|
||||
// Per-metric queryKey so two tiles for two different metrics never
|
||||
// share a cache slot (HealthLog convention — see
|
||||
// `feedback_react_query_key_collision`).
|
||||
queryKey: ["personal-records", "by-metric", metricType],
|
||||
queryKey: queryKeys.personalRecordsByMetric(metricType),
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const data = await apiGet<PersonalRecordRow[] | null>(
|
||||
|
||||
@@ -1508,7 +1508,7 @@ function DayDrillDown({
|
||||
const { t, locale } = useTranslations();
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ["measurement-drilldown", type, dayKey],
|
||||
queryKey: queryKeys.measurementDrilldown(type, dayKey),
|
||||
queryFn: async () => {
|
||||
const params = new URLSearchParams();
|
||||
params.set("type", type);
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
} from "@/lib/medications/side-effects/taxonomy";
|
||||
import { MedicationDetailSection } from "@/components/medications/medication-detail-section";
|
||||
import { apiDelete, apiPost } from "@/lib/api/api-fetch";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
/**
|
||||
* v1.4.25 W19d — GLP-1 side-effect section for the medication detail
|
||||
@@ -114,12 +115,7 @@ export function SideEffectsSection({ medicationId }: SideEffectsSectionProps) {
|
||||
const [footerEl, setFooterEl] = useState<HTMLDivElement | null>(null);
|
||||
const formId = useId();
|
||||
|
||||
const listKey = [
|
||||
"medications",
|
||||
medicationId,
|
||||
"side-effects",
|
||||
"list",
|
||||
] as const;
|
||||
const listKey = queryKeys.medicationSideEffects(medicationId);
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: listKey,
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { apiGet, apiPost, apiPut } from "@/lib/api/api-fetch";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import {
|
||||
AnamnesisCard,
|
||||
buildAnamnesisAboutMeBody,
|
||||
@@ -141,7 +142,7 @@ export function BaselineForm() {
|
||||
}
|
||||
}
|
||||
await apiPost("/api/onboarding/step", { step: 4 });
|
||||
await queryClient.invalidateQueries({ queryKey: ["auth"] });
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.auth() });
|
||||
router.push("/onboarding/4");
|
||||
} catch (err) {
|
||||
const message =
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiPost } from "@/lib/api/api-fetch";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
/**
|
||||
* v1.4.25 W14b-Content — onboarding step 1 (goals).
|
||||
@@ -127,7 +128,7 @@ export function GoalsChipPicker({ userId: _userId }: GoalsChipPickerProps) {
|
||||
step: 2,
|
||||
goals: Array.from(selected),
|
||||
});
|
||||
await queryClient.invalidateQueries({ queryKey: ["auth"] });
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.auth() });
|
||||
router.push("/onboarding/2");
|
||||
} catch (err) {
|
||||
const message =
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiPost } from "@/lib/api/api-fetch";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
/**
|
||||
* Onboarding step 2 (source).
|
||||
@@ -105,7 +106,7 @@ export function SourceCardGrid() {
|
||||
setAdvancing(true);
|
||||
try {
|
||||
await apiPost("/api/onboarding/step", { step: 3 });
|
||||
await queryClient.invalidateQueries({ queryKey: ["auth"] });
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.auth() });
|
||||
router.push("/onboarding/3");
|
||||
} catch (err) {
|
||||
const message =
|
||||
|
||||
@@ -22,6 +22,7 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
import { scrollBehaviorForUser } from "@/lib/motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiPost } from "@/lib/api/api-fetch";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { DISCLAIMER_VERSION } from "@/lib/onboarding/disclaimer";
|
||||
|
||||
/**
|
||||
@@ -152,7 +153,7 @@ export function WelcomeCarousel() {
|
||||
});
|
||||
}
|
||||
await apiPost("/api/onboarding/step", { step: 1 });
|
||||
await queryClient.invalidateQueries({ queryKey: ["auth"] });
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.auth() });
|
||||
router.push("/onboarding/1");
|
||||
} catch (err) {
|
||||
const message =
|
||||
|
||||
@@ -239,7 +239,7 @@ export function GettingStartedChecklist() {
|
||||
});
|
||||
|
||||
const { data: notificationsData } = useQuery<NotificationsPreferences>({
|
||||
queryKey: ["notifications", "preferences"],
|
||||
queryKey: queryKeys.notificationsPreferences(),
|
||||
queryFn: async () => {
|
||||
return apiGet("/api/notifications/preferences");
|
||||
},
|
||||
|
||||
@@ -45,6 +45,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { apiPost } from "@/lib/api/api-fetch";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { OnboardingTour } from "./tour";
|
||||
|
||||
const POST_WIZARD_GRACE_MS = 1500;
|
||||
@@ -371,7 +372,7 @@ export function TourLauncher({ ready = true }: TourLauncherProps) {
|
||||
if (user?.id) writeSessionDismissed(user.id);
|
||||
try {
|
||||
await apiPost("/api/onboarding/tour", { completed: true, outcome });
|
||||
await queryClient.invalidateQueries({ queryKey: ["auth"] });
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.auth() });
|
||||
} catch {
|
||||
// Swallow — the session-dismiss flag prevents a re-open.
|
||||
// Next visit will retry via the same code path.
|
||||
|
||||
@@ -144,7 +144,7 @@ function TargetEditSheetBody({
|
||||
// tone vs. the persisted override). The body only mounts when the
|
||||
// dialog is open, so we don't need a separate `enabled` flag.
|
||||
const { data: thresholdsData } = useQuery({
|
||||
queryKey: ["user", "thresholds"],
|
||||
queryKey: queryKeys.userThresholds(),
|
||||
queryFn: async () => {
|
||||
return apiGet<ThresholdsApiResponse>("/api/user/thresholds");
|
||||
},
|
||||
@@ -227,10 +227,10 @@ function TargetEditSheetBody({
|
||||
await apiPut("/api/user/thresholds", payload);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["user", "thresholds"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.userThresholds() });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.insightsTargets() });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.analytics() });
|
||||
queryClient.invalidateQueries({ queryKey: ["insights"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.insightsRoot() });
|
||||
toast.success(t("targets.edit.saveSuccess"));
|
||||
onOpenChange(false);
|
||||
},
|
||||
@@ -253,10 +253,10 @@ function TargetEditSheetBody({
|
||||
await apiDelete(`/api/user/thresholds?metric=${metric}`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["user", "thresholds"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.userThresholds() });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.insightsTargets() });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.analytics() });
|
||||
queryClient.invalidateQueries({ queryKey: ["insights"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.insightsRoot() });
|
||||
toast.success(t("targets.edit.resetSuccess"));
|
||||
onOpenChange(false);
|
||||
},
|
||||
|
||||
@@ -32,6 +32,8 @@ import {
|
||||
type QueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
const DB_NAME = "healthlog-query-cache";
|
||||
const STORE = "kv";
|
||||
const KEY = "react-query";
|
||||
@@ -143,7 +145,9 @@ interface PersistedPayload {
|
||||
|
||||
/** Read the current account id from the live `["auth","me"]` cache entry. */
|
||||
function currentUserId(queryClient: QueryClient): string | null {
|
||||
const me = queryClient.getQueryData<{ id?: string } | null>(["auth", "me"]);
|
||||
const me = queryClient.getQueryData<{ id?: string } | null>(
|
||||
queryKeys.authMe(),
|
||||
);
|
||||
return me?.id ?? null;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,4 +13,18 @@ export const coachKeys = {
|
||||
coachNudgeStatus: () => ["coach-nudge-status"] as const,
|
||||
/** v1.18.6 (CCH-03) — mark-Coach-seen mutation (clears the FAB unread dot). */
|
||||
coachMarkSeen: () => ["coach-mark-seen"] as const,
|
||||
/**
|
||||
* v1.18.7 — the Coach conversation rail list (`GET /api/insights/chat`).
|
||||
* Previously a hook-local bare `["coachConversations"]`; centralised so
|
||||
* the optimistic delete and the streaming hook's post-turn invalidation
|
||||
* share one factory-routed key.
|
||||
*/
|
||||
coachConversations: () => ["coachConversations"] as const,
|
||||
/**
|
||||
* v1.18.7 — one decrypted Coach conversation (`GET /api/insights/chat/[id]`).
|
||||
* Keyed on the conversation id so two open threads never share a cache slot;
|
||||
* the streaming hook invalidates this slot once the persisted twin lands.
|
||||
*/
|
||||
coachConversation: (id: string | null) =>
|
||||
["coachConversation", id] as const,
|
||||
};
|
||||
@@ -126,6 +126,25 @@ export const measurementKeys = {
|
||||
measurementDiversity: (type: string) =>
|
||||
["measurement-diversity", type] as const,
|
||||
|
||||
/**
|
||||
* v1.18.7 — per-day drill-down list for one `MeasurementType`
|
||||
* (`GET /api/measurements?type=&dayKey=`), opened from a values-list
|
||||
* day row. Keyed by `(type, dayKey)` so each expanded day caches its own
|
||||
* slot. Distinct `["measurement-drilldown"]` prefix (not `["measurements"]`)
|
||||
* because the day list is a stable historical slice that the drain has
|
||||
* already settled — it does not need a measurement-write fan-out.
|
||||
*/
|
||||
measurementDrilldown: (type: string, dayKey: string) =>
|
||||
["measurement-drilldown", type, dayKey] as const,
|
||||
|
||||
/**
|
||||
* v1.18.7 — per-metric personal-records read (`GET /api/personal-records`)
|
||||
* backing the decorative PR badge on a metric tile. Per-metric key so two
|
||||
* tiles never share a cache slot; its own `["personal-records"]` prefix.
|
||||
*/
|
||||
personalRecordsByMetric: (metric: string) =>
|
||||
["personal-records", "by-metric", metric] as const,
|
||||
|
||||
/**
|
||||
* v1.18.7 (Wave E) — bounded last-7 readings for one `MeasurementType`,
|
||||
* powering the discreet 7-day trend strip on a Vorsorge card. Rides under
|
||||
|
||||
@@ -8,6 +8,22 @@ export const medicationKeys = {
|
||||
medicationDetail: (id: string) => ["medications", id] as const,
|
||||
medicationComplianceChart: (medicationId: string) =>
|
||||
["compliance-chart-inline", medicationId] as const,
|
||||
/**
|
||||
* v1.18.7 — the bare `["compliance-chart-inline"]` prefix, for fan-out
|
||||
* invalidation across every per-medication inline compliance tile in one
|
||||
* tick (e.g. a dashboard quick-add intake). `medicationComplianceChart(id)`
|
||||
* above keys an individual tile; this routes the parent prefix through the
|
||||
* factory so the fan-out is not a bare array at the call site.
|
||||
*/
|
||||
complianceChartInline: () => ["compliance-chart-inline"] as const,
|
||||
/**
|
||||
* v1.18.7 — per-medication side-effects list, last 30 days
|
||||
* (`GET /api/medications/[id]/side-effects`). Rides under the
|
||||
* `["medications", id, …]` prefix so a CRUD mutation reaches it through
|
||||
* `medicationDependentKeys`.
|
||||
*/
|
||||
medicationSideEffects: (medicationId: string) =>
|
||||
["medications", medicationId, "side-effects", "list"] as const,
|
||||
/**
|
||||
* v1.4.42 W3-QUERYKEY-LONGTAIL — per-medication compliance KPI used
|
||||
* by `medication-card` + `glp1-medication-card`. Centralising lets
|
||||
|
||||
Reference in new issue
Block a user