refactor(query-keys): route long-tail surfaces through the factory

The settings / medications / admin / hooks tree still declared
bare-literal `queryKey: [ … ]` arrays at ~70 call sites. Centralise
them through `queryKeys.<entry>()` so mutation-invalidation prefixes
stay symmetrical and a future rename only touches one location.

New factory entries cover per-medication compliance / titration /
cadence / glp1-details / drug-level-chart / intake-list, the
Withings status read, the paginated admin audit-log filter, and the
workouts-recent list opts. Existing entries cover everything else
(passkeys, tokens, settings, telegram, integrations, research-mode,
admin status / users / backups / feedback / host-metrics / app-logs /
ai-quality / assistant-flags, coach-prefs, feature-flags, etc.).

Lockstep updates:

- `eslint-plugins/healthlog/queryKey-factory.js` extends
  `GUARDED_DIRECTORIES` to cover `src/components/{settings,
  medications,admin}` + `src/hooks`, plus the three guarded files
  under `src/app/medications/**` and `src/app/targets/page.tsx`.
- `src/lib/__tests__/query-keys.test.ts` mirrors the guard's
  `guardedRoots` and pins the shape of every new factory entry.

Tests stay green: 4 736 unit; the factory walker reports zero
bare-literal `queryKey:` in any newly-guarded surface.
This commit is contained in:
Marc-André Bombeck committed 2026-05-21 19:04:22 +02:00
1 parent 6850818271
commit e2018b2d15
43 files changed
+332 -118

No files matched your search

+2 -1
View File
@@ -13,6 +13,7 @@ import { IntakeHistoryListV2 } from "@/components/medications/intake-history-lis
import { ArrowLeft, Loader2 } from "lucide-react";
import Link from "next/link";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
export default function IntakeHistoryPage({
params,
@@ -31,7 +32,7 @@ export default function IntakeHistoryPage({
}, [authLoading, isAuthenticated, router]);
const { data: medication, isLoading: medLoading } = useQuery({
queryKey: ["medications", id],
queryKey: queryKeys.medicationDetail(id),
queryFn: async () => {
const res = await fetch(`/api/medications/${id}`);
if (!res.ok) throw new Error("Failed to fetch");
+6 -2
View File
@@ -4,7 +4,11 @@ import { useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useAuth } from "@/hooks/use-auth";
import { useTranslations } from "@/lib/i18n/context";
import { invalidateKeys, medicationDependentKeys } from "@/lib/query-keys";
import {
invalidateKeys,
medicationDependentKeys,
queryKeys,
} from "@/lib/query-keys";
import { parseScheduleRecurrence } from "@/lib/medication-schedule";
import { MedicationForm } from "@/components/medications/medication-form";
import { MedicationCard } from "@/components/medications/medication-card";
@@ -90,7 +94,7 @@ export default function MedicationsPage() {
isError,
refetch: refetchMedications,
} = useQuery({
queryKey: ["medications"],
queryKey: queryKeys.medications(),
queryFn: async () => {
const res = await fetch("/api/medications");
if (!res.ok) throw new Error("Failed to fetch");
+5 -5
View File
@@ -6,6 +6,7 @@ import { AlertCircle, Loader2 } from "lucide-react";
import { useAuth } from "@/hooks/use-auth";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import { Card, CardContent } from "@/components/ui/card";
import { convertGlucose, resolveGlucoseUnit } from "@/lib/glucose";
import {
@@ -46,9 +47,8 @@ interface TargetsResponse {
/**
* v1.4.25 W3e — provider-chain status used to gate the per-card Coach
* CTA. Returns true when at least one provider is configured AND
* enabled. The Settings → AI surface owns the same query
* (`useQuery({ queryKey: ["insights", "provider-chain"] })`) so the
* cache is shared.
* enabled. The Settings → AI surface owns the same query (via
* `queryKeys.insightsProviderChain()`) so the cache is shared.
*/
interface ProviderChainStatus {
activeProvider: string | null;
@@ -132,7 +132,7 @@ export default function TargetsPage() {
const { isAuthenticated } = useAuth();
const { data, isLoading } = useQuery({
queryKey: ["insights", "targets"],
queryKey: queryKeys.insightsTargets(),
queryFn: async () => {
const res = await fetch("/api/insights/targets");
if (!res.ok) throw new Error(t("targets.loadError"));
@@ -146,7 +146,7 @@ export default function TargetsPage() {
// user has no configured provider, `activeProvider` is null and the
// per-card Coach CTA hides entirely (no broken-button state).
const { data: chainStatus } = useQuery({
queryKey: ["insights", "provider-chain"],
queryKey: queryKeys.insightsProviderChain(),
queryFn: async () => {
const res = await fetch("/api/insights/provider-chain");
if (!res.ok) return null;