mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
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:
1 parent
6850818271
commit
e2018b2d15
43 files changed
+332
-118
No files matched your search
@@ -53,6 +53,13 @@ const GUARDED_DIRECTORIES = [
|
||||
// at the same boundary the unit-test walker does.
|
||||
"src/app/auth",
|
||||
"src/app/notifications",
|
||||
// v1.4.42 W3-QUERYKEY-LONGTAIL — the settings / medications /
|
||||
// admin / hooks surface is now factory-only. Every long-tail
|
||||
// bare-literal flagged by audit-H1 routes through `queryKeys.<entry>()`.
|
||||
"src/components/settings",
|
||||
"src/components/medications",
|
||||
"src/components/admin",
|
||||
"src/hooks",
|
||||
];
|
||||
|
||||
// Individual guarded files outside the directories above.
|
||||
@@ -61,6 +68,11 @@ const GUARDED_FILES = [
|
||||
"src/hooks/use-auth.ts",
|
||||
// v1.4.41 W-FRONTEND-FACTORY — about-section migrated.
|
||||
"src/components/settings/about-section.tsx",
|
||||
// v1.4.42 W3-QUERYKEY-LONGTAIL — medications + targets pages now
|
||||
// route every read through the factory.
|
||||
"src/app/medications/page.tsx",
|
||||
"src/app/medications/[id]/history/page.tsx",
|
||||
"src/app/targets/page.tsx",
|
||||
];
|
||||
|
||||
function isGuarded(filename) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { toast } from "sonner";
|
||||
import { Cpu, Fingerprint, Globe, KeyRound } from "lucide-react";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { PasswordInput as SharedPasswordInput } from "@/components/ui/password-input";
|
||||
|
||||
/**
|
||||
@@ -225,7 +226,7 @@ export function SettingsToggle({
|
||||
|
||||
export function useAdminSettings() {
|
||||
return useQuery({
|
||||
queryKey: ["admin", "settings"],
|
||||
queryKey: queryKeys.adminSettings(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/admin/settings");
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
@@ -249,7 +250,7 @@ export function useUpdateSettings() {
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["admin", "settings"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.adminSettings() });
|
||||
toast.success(t("common.saved"));
|
||||
},
|
||||
onError: (err) => {
|
||||
@@ -523,7 +524,7 @@ export function auditLogCsvHeaderLabels(
|
||||
|
||||
export function useSystemStatus() {
|
||||
return useQuery({
|
||||
queryKey: ["admin", "status"],
|
||||
queryKey: queryKeys.adminStatus(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/admin/status");
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Sparkles, Loader2 } from "lucide-react";
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
interface FeedbackBucket {
|
||||
severity: string;
|
||||
@@ -63,7 +64,7 @@ export function AiQualitySection() {
|
||||
const fmt = useFormatters();
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["admin", "ai-quality"],
|
||||
queryKey: queryKeys.adminAiQuality(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/admin/ai-quality");
|
||||
const json = (await res.json()) as AiQualityResponse;
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { formatDate, formatDateTime } from "@/lib/format";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { type ApiTokenInfo } from "./_shared";
|
||||
|
||||
/**
|
||||
@@ -147,7 +148,7 @@ export function ApiTokenOverviewSection() {
|
||||
// one card the toggle hides the entire surface. the maintainer reported it
|
||||
// as "sinnlos" — gone.
|
||||
const { data: tokens, isLoading } = useQuery({
|
||||
queryKey: ["admin", "tokens"],
|
||||
queryKey: queryKeys.adminTokens(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/admin/tokens");
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import type { WideEvent, LogLevel } from "@/lib/logging/types";
|
||||
|
||||
type RangePreset = "15m" | "1h" | "6h" | "all";
|
||||
@@ -100,7 +101,7 @@ export function AppLogPreviewSection() {
|
||||
}, [traceId, actionFilter, level, range]);
|
||||
|
||||
const { data, isLoading, refetch, isFetching } = useQuery({
|
||||
queryKey: ["admin", "app-logs", traceId, actionFilter, level, range],
|
||||
queryKey: queryKeys.adminAppLogs(traceId, actionFilter, level, range),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`/api/admin/app-logs?${params.toString()}`);
|
||||
if (!res.ok) throw new Error("Failed to load app logs");
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Sparkles } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { SettingsToggle, getApiErrorMessage } from "./_shared";
|
||||
|
||||
/**
|
||||
@@ -45,7 +46,7 @@ interface AssistantFlagsResponse {
|
||||
|
||||
function useAssistantFlags() {
|
||||
return useQuery({
|
||||
queryKey: ["admin", "settings", "assistant-flags"],
|
||||
queryKey: queryKeys.adminAssistantFlags(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/admin/settings/assistant-flags");
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
@@ -73,12 +74,12 @@ function useUpdateAssistantFlags() {
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
client.setQueryData(
|
||||
["admin", "settings", "assistant-flags"],
|
||||
queryKeys.adminAssistantFlags(),
|
||||
data,
|
||||
);
|
||||
// Bust the runtime `/api/feature-flags` cache so the operator
|
||||
// sees the toggled surface react within the same session.
|
||||
client.invalidateQueries({ queryKey: ["feature-flags"] });
|
||||
client.invalidateQueries({ queryKey: queryKeys.featureFlags() });
|
||||
toast.success(t("common.saved"));
|
||||
},
|
||||
onError: (err) => {
|
||||
@@ -88,7 +89,7 @@ function useUpdateAssistantFlags() {
|
||||
: t("admin.settingsSaveError"),
|
||||
);
|
||||
client.invalidateQueries({
|
||||
queryKey: ["admin", "settings", "assistant-flags"],
|
||||
queryKey: queryKeys.adminAssistantFlags(),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -41,6 +41,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import type { BackupRow, BackupsList } from "@/types/backups";
|
||||
import { getApiErrorMessage } from "./_shared";
|
||||
|
||||
@@ -172,7 +173,7 @@ export function BackupsSection() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ["admin", "backups"],
|
||||
queryKey: queryKeys.adminBackups(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/admin/backups");
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
@@ -194,7 +195,7 @@ export function BackupsSection() {
|
||||
// shows up without leaving the user wondering whether the click
|
||||
// did anything.
|
||||
setTimeout(() => {
|
||||
queryClient.invalidateQueries({ queryKey: ["admin", "backups"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.adminBackups() });
|
||||
}, 2000);
|
||||
},
|
||||
onError: (err) => {
|
||||
@@ -245,7 +246,7 @@ export function BackupsSection() {
|
||||
toast.success(
|
||||
t("admin.section.backups.uploadSuccess", { count: String(total) }),
|
||||
);
|
||||
queryClient.invalidateQueries({ queryKey: ["admin", "backups"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.adminBackups() });
|
||||
},
|
||||
onError: (err) => {
|
||||
toast.error(
|
||||
@@ -290,7 +291,7 @@ export function BackupsSection() {
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success(t("admin.section.backups.restoreSuccess"));
|
||||
queryClient.invalidateQueries({ queryKey: ["admin", "backups"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.adminBackups() });
|
||||
// Restore touches every personal-data table; nuke the broader
|
||||
// cache so dashboards / lists rebuild against the new state.
|
||||
queryClient.invalidateQueries();
|
||||
|
||||
@@ -22,6 +22,7 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { Loader2, Sparkles } from "lucide-react";
|
||||
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
interface CoachFeedbackBucket {
|
||||
promptVersion: string;
|
||||
@@ -55,7 +56,7 @@ export function CoachFeedbackSection() {
|
||||
const fmt = useFormatters();
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["admin", "coach-feedback"],
|
||||
queryKey: queryKeys.adminCoachFeedback(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/admin/ai-quality");
|
||||
const json = (await res.json()) as CoachFeedbackResponse;
|
||||
|
||||
@@ -30,6 +30,7 @@ import { Label } from "@/components/ui/label";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import {
|
||||
FEEDBACK_STATUS_TABS,
|
||||
type FeedbackCategoryType,
|
||||
@@ -50,7 +51,7 @@ export function FeedbackInboxSection() {
|
||||
const [selected, setSelected] = useState<FeedbackItem | null>(null);
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["admin", "feedback", activeStatus],
|
||||
queryKey: queryKeys.adminFeedback(activeStatus),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`/api/admin/feedback?status=${activeStatus}&limit=100`,
|
||||
@@ -63,7 +64,7 @@ export function FeedbackInboxSection() {
|
||||
const counts = data?.meta?.countsByStatus ?? {};
|
||||
|
||||
function refresh() {
|
||||
queryClient.invalidateQueries({ queryKey: ["admin", "feedback"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.adminFeedbackRoot() });
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
} from "recharts";
|
||||
import { useMemo } from "react";
|
||||
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
interface HostMetricsApiSample {
|
||||
capturedAt: string;
|
||||
@@ -130,7 +131,7 @@ export function HostMetricsChart() {
|
||||
const fmt = useFormatters();
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ["admin", "host-metrics", "2h"],
|
||||
queryKey: queryKeys.adminHostMetrics("2h"),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/admin/host-metrics?since=2h");
|
||||
if (!res.ok) throw new Error("Failed to load host metrics");
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { toCSV } from "@/lib/export";
|
||||
import { formatInUserTz, DEFAULT_TIMEZONE } from "@/lib/tz/format";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
@@ -114,10 +115,7 @@ export function LoginOverviewSection() {
|
||||
}, [page, perPage, actor, actionFilter, target, range]);
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: [
|
||||
"admin",
|
||||
"audit-log",
|
||||
"filtered",
|
||||
queryKey: queryKeys.adminAuditLogFiltered({
|
||||
filter,
|
||||
page,
|
||||
perPage,
|
||||
@@ -125,7 +123,7 @@ export function LoginOverviewSection() {
|
||||
actionFilter,
|
||||
target,
|
||||
range,
|
||||
],
|
||||
}),
|
||||
queryFn: async () => {
|
||||
const params = new URLSearchParams(queryParams);
|
||||
// Quick-filter pill stacks on top of the new filters.
|
||||
@@ -140,7 +138,7 @@ export function LoginOverviewSection() {
|
||||
|
||||
// Distinct actions for the dropdown — populated lazily.
|
||||
const { data: actionsData } = useQuery({
|
||||
queryKey: ["admin", "audit-log", "actions"],
|
||||
queryKey: queryKeys.adminAuditActions(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/admin/audit-log/actions");
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { type AdminAuditEntry, useAuthActionLabels } from "./_shared";
|
||||
|
||||
const PREVIEW_LIMIT = 10;
|
||||
@@ -34,7 +35,7 @@ export function RecentAuditPreview() {
|
||||
const { t } = useTranslations();
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ["admin", "audit-log", "overview-preview"],
|
||||
queryKey: queryKeys.adminAuditOverview(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`/api/admin/audit-log?limit=${PREVIEW_LIMIT}&filter=auth`,
|
||||
|
||||
@@ -22,6 +22,7 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { StatusItem, useSystemStatus } from "./_shared";
|
||||
|
||||
interface VersionResponse {
|
||||
@@ -33,7 +34,7 @@ interface VersionResponse {
|
||||
|
||||
function useOfflineGeoState() {
|
||||
return useQuery({
|
||||
queryKey: ["public", "version"],
|
||||
queryKey: queryKeys.publicVersion(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/version");
|
||||
if (!res.ok) throw new Error("Failed to load version");
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { StatusItem, useSystemStatus } from "./_shared";
|
||||
|
||||
interface VersionResponse {
|
||||
@@ -36,7 +37,7 @@ interface VersionResponse {
|
||||
|
||||
function useVersion() {
|
||||
return useQuery({
|
||||
queryKey: ["public", "version"],
|
||||
queryKey: queryKeys.publicVersion(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/version");
|
||||
if (!res.ok) throw new Error("Failed to load version");
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { formatDate } from "@/lib/format";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { type AdminUser, PasswordInput } from "./_shared";
|
||||
|
||||
/**
|
||||
@@ -49,7 +50,7 @@ export function UserManagementSection() {
|
||||
const [logoutTarget, setLogoutTarget] = useState<AdminUser | null>(null);
|
||||
|
||||
const { data: users } = useQuery({
|
||||
queryKey: ["admin", "users"],
|
||||
queryKey: queryKeys.adminUsers(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/admin/users");
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
@@ -81,7 +82,7 @@ export function UserManagementSection() {
|
||||
if (!res.ok) throw new Error(json.error || t("common.error"));
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["admin", "users"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.adminUsers() });
|
||||
setEditingUser(null);
|
||||
toast.success(t("common.saved"));
|
||||
},
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import {
|
||||
computeOneCompartment,
|
||||
type DoseEvent,
|
||||
@@ -129,7 +130,7 @@ export function DrugLevelChart({ medication, asOf }: DrugLevelChartProps) {
|
||||
|
||||
const { data: researchMode, isLoading: rmLoading } =
|
||||
useQuery<ResearchModeStatus | null>({
|
||||
queryKey: ["research-mode"],
|
||||
queryKey: queryKeys.researchMode(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/auth/me/research-mode");
|
||||
if (!res.ok) return null;
|
||||
@@ -146,7 +147,7 @@ export function DrugLevelChart({ medication, asOf }: DrugLevelChartProps) {
|
||||
|
||||
const { data: details, isLoading: detailsLoading } =
|
||||
useQuery<Glp1DetailsResponse | null>({
|
||||
queryKey: ["medications", medication.id, "glp1-details"],
|
||||
queryKey: queryKeys.medicationGlp1Details(medication.id),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`/api/medications/${medication.id}/glp1`);
|
||||
if (!res.ok) return null;
|
||||
@@ -160,7 +161,7 @@ export function DrugLevelChart({ medication, asOf }: DrugLevelChartProps) {
|
||||
});
|
||||
|
||||
const { data: intakeEnvelope, isLoading: intakeLoading } = useQuery({
|
||||
queryKey: ["medications", medication.id, "intake", "drug-level-chart"],
|
||||
queryKey: queryKeys.medicationIntakeDrugLevelChart(medication.id),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`/api/medications/${medication.id}/intake?limit=20&sortBy=takenAt&sortDir=desc`,
|
||||
|
||||
@@ -45,6 +45,7 @@ import { AlertTriangle, BookOpenCheck, Loader2 } from "lucide-react";
|
||||
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
export interface ResearchModeAcknowledgmentDialogProps {
|
||||
open: boolean;
|
||||
@@ -94,7 +95,7 @@ export function ResearchModeAcknowledgmentDialog({
|
||||
onSuccess: () => {
|
||||
setErrorMessage(null);
|
||||
toast.success(t("medications.researchMode.dialog.successToast"));
|
||||
queryClient.invalidateQueries({ queryKey: ["research-mode"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.researchMode() });
|
||||
onAcknowledged?.();
|
||||
onOpenChange(false);
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Loader2, BellRing, BellOff, Pencil } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { MedicationDetailSection } from "@/components/medications/medication-detail-section";
|
||||
|
||||
/**
|
||||
@@ -92,7 +93,7 @@ export function SchedulingSection({
|
||||
const fmt = useFormatters();
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ["medications", medicationId, "cadence"],
|
||||
queryKey: queryKeys.medicationCadence(medicationId),
|
||||
queryFn: async (): Promise<CadenceResponse> => {
|
||||
const res = await fetch(
|
||||
`/api/medications/${medicationId}/cadence?days=30`,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { Loader2, ExternalLink } from "lucide-react";
|
||||
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { MedicationDetailSection } from "@/components/medications/medication-detail-section";
|
||||
|
||||
/**
|
||||
@@ -62,7 +63,7 @@ export function TitrationSection({ medicationId }: TitrationSectionProps) {
|
||||
const { t } = useTranslations();
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ["medications", medicationId, "titration"],
|
||||
queryKey: queryKeys.medicationTitration(medicationId),
|
||||
queryFn: async (): Promise<TitrationResponse> => {
|
||||
const res = await fetch(`/api/medications/${medicationId}/titration`);
|
||||
if (!res.ok) {
|
||||
|
||||
@@ -29,7 +29,11 @@ import {
|
||||
import { MedicationCardHeader } from "@/components/medications/MedicationCardHeader";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import { invalidateKeys, medicationDependentKeys } from "@/lib/query-keys";
|
||||
import {
|
||||
invalidateKeys,
|
||||
medicationDependentKeys,
|
||||
queryKeys,
|
||||
} from "@/lib/query-keys";
|
||||
import { formatDateTime, formatTime } from "@/lib/format";
|
||||
import { formatTimeWindowRange } from "@/lib/time-window-format";
|
||||
import { getMedicationCategoryLabel } from "@/lib/medications/category-label";
|
||||
@@ -179,7 +183,7 @@ export function Glp1MedicationCard({
|
||||
const [, forceUpdate] = useReducer((x: number) => x + 1, 0);
|
||||
|
||||
const { data: compliance } = useQuery({
|
||||
queryKey: ["medications", medication.id, "compliance"],
|
||||
queryKey: queryKeys.medicationCompliance(medication.id),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`/api/medications/${medication.id}/compliance`);
|
||||
if (!res.ok) return null;
|
||||
@@ -194,7 +198,7 @@ export function Glp1MedicationCard({
|
||||
// medication card so the take-now / overdue / very-overdue pill
|
||||
// tiers identically on both surfaces.
|
||||
const { data: thresholds } = useQuery({
|
||||
queryKey: ["settings", "reminder-thresholds"],
|
||||
queryKey: queryKeys.settingsReminderThresholds(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/settings/reminder-thresholds");
|
||||
if (!res.ok) return null;
|
||||
@@ -208,7 +212,7 @@ export function Glp1MedicationCard({
|
||||
// pen inventory). Lives behind a dedicated endpoint so the standard
|
||||
// medications grid loads in parallel without paying for it.
|
||||
const { data: details } = useQuery({
|
||||
queryKey: ["medications", medication.id, "glp1-details"],
|
||||
queryKey: queryKeys.medicationGlp1Details(medication.id),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`/api/medications/${medication.id}/glp1`);
|
||||
if (!res.ok) return null;
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
@@ -111,13 +112,13 @@ export function IntakeHistoryListV2({
|
||||
const offset = page * pageSize;
|
||||
|
||||
const { data, isLoading, isError } = useQuery<IntakeListResponse>({
|
||||
queryKey: [
|
||||
"medications",
|
||||
medicationId,
|
||||
"intake",
|
||||
"list",
|
||||
{ sortBy, sortDir, limit: pageSize, offset, status: STATUS_FILTER },
|
||||
],
|
||||
queryKey: queryKeys.medicationIntakeList(medicationId, {
|
||||
sortBy,
|
||||
sortDir,
|
||||
limit: pageSize,
|
||||
offset,
|
||||
status: STATUS_FILTER,
|
||||
}),
|
||||
queryFn: async () => {
|
||||
const params = new URLSearchParams({
|
||||
sortBy,
|
||||
|
||||
@@ -29,7 +29,11 @@ import {
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import { invalidateKeys, medicationDependentKeys } from "@/lib/query-keys";
|
||||
import {
|
||||
invalidateKeys,
|
||||
medicationDependentKeys,
|
||||
queryKeys,
|
||||
} from "@/lib/query-keys";
|
||||
|
||||
interface Schedule {
|
||||
id: string;
|
||||
@@ -128,7 +132,7 @@ export function MedicationCard({ medication, onEdit }: MedicationCardProps) {
|
||||
const [intakeLoading, setIntakeLoading] = useState<string | null>(null);
|
||||
|
||||
const { data: compliance } = useQuery({
|
||||
queryKey: ["medications", medication.id, "compliance"],
|
||||
queryKey: queryKeys.medicationCompliance(medication.id),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`/api/medications/${medication.id}/compliance`);
|
||||
if (!res.ok) return null;
|
||||
@@ -141,7 +145,7 @@ export function MedicationCard({ medication, onEdit }: MedicationCardProps) {
|
||||
const [, forceUpdate] = useReducer((x: number) => x + 1, 0);
|
||||
|
||||
const { data: thresholds } = useQuery({
|
||||
queryKey: ["settings", "reminder-thresholds"],
|
||||
queryKey: queryKeys.settingsReminderThresholds(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/settings/reminder-thresholds");
|
||||
if (!res.ok) return null;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Loader2, RotateCcw } from "lucide-react";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
interface PhaseConfigData {
|
||||
greenValue: number;
|
||||
@@ -76,7 +77,7 @@ export function PhaseConfigDialog({
|
||||
const [statusMessage, setStatusMessage] = useState<string | null>(null);
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["phase-config", medicationId],
|
||||
queryKey: queryKeys.medicationPhaseConfig(medicationId),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`/api/medications/${medicationId}/phase-config`);
|
||||
const json = await res.json();
|
||||
@@ -101,7 +102,7 @@ export function PhaseConfigDialog({
|
||||
onSuccess: () => {
|
||||
setLocalForm(null);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["phase-config", medicationId],
|
||||
queryKey: queryKeys.medicationPhaseConfig(medicationId),
|
||||
});
|
||||
setStatusMessage(t("medications.phaseSaved"));
|
||||
setTimeout(() => setStatusMessage(null), 2000);
|
||||
@@ -119,7 +120,7 @@ export function PhaseConfigDialog({
|
||||
onSuccess: () => {
|
||||
setLocalForm(null);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["phase-config", medicationId],
|
||||
queryKey: queryKeys.medicationPhaseConfig(medicationId),
|
||||
});
|
||||
setStatusMessage(t("medications.phaseReset"));
|
||||
setTimeout(() => setStatusMessage(null), 2000);
|
||||
|
||||
@@ -44,6 +44,7 @@ import { formatDate } from "@/lib/format";
|
||||
import { locales, localeLabels, type Locale } from "@/lib/i18n/config";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { describePasskeyError } from "@/lib/passkey-errors";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { TimezonePicker } from "@/components/settings/timezone-picker";
|
||||
import { detectBrowserTimezone, DEFAULT_TIMEZONE } from "@/lib/tz/format";
|
||||
|
||||
@@ -726,7 +727,7 @@ function PasskeyListSection({ isAuthenticated }: { isAuthenticated: boolean }) {
|
||||
const [deleteMsg, setDeleteMsg] = useState<string | null>(null);
|
||||
|
||||
const { data: passkeys } = useQuery({
|
||||
queryKey: ["passkeys"],
|
||||
queryKey: queryKeys.passkeys(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/auth/passkeys");
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
@@ -742,7 +743,7 @@ function PasskeyListSection({ isAuthenticated }: { isAuthenticated: boolean }) {
|
||||
if (!res.ok) throw new Error(json.error || t("common.error"));
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["passkeys"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.passkeys() });
|
||||
setDeleteMsg(null);
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
|
||||
@@ -20,6 +20,7 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { ResearchModeAcknowledgmentDialog } from "@/components/medications/ResearchModeAcknowledgmentDialog";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import {
|
||||
type ResearchModeStatus,
|
||||
researchModeGateState,
|
||||
@@ -102,7 +103,7 @@ function ResearchModeCard() {
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const { data: status, isLoading } = useQuery<ResearchModeStatus | null>({
|
||||
queryKey: ["research-mode"],
|
||||
queryKey: queryKeys.researchMode(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/auth/me/research-mode");
|
||||
if (!res.ok) return null;
|
||||
@@ -140,7 +141,7 @@ function ResearchModeCard() {
|
||||
setErrorMessage(t("settings.researchMode.disableError"));
|
||||
return;
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ["research-mode"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.researchMode() });
|
||||
} catch {
|
||||
setErrorMessage(t("settings.researchMode.disableError"));
|
||||
} finally {
|
||||
|
||||
@@ -37,6 +37,7 @@ import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
interface InsightsSettings {
|
||||
codexStatus: string;
|
||||
@@ -186,7 +187,7 @@ function AiInsightsCard({ isAuthenticated }: { isAuthenticated: boolean }) {
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const { data: insightsSettings } = useQuery({
|
||||
queryKey: ["insights", "settings"],
|
||||
queryKey: queryKeys.insightsSettings(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/insights/settings");
|
||||
if (!res.ok) return null;
|
||||
@@ -197,7 +198,7 @@ function AiInsightsCard({ isAuthenticated }: { isAuthenticated: boolean }) {
|
||||
});
|
||||
|
||||
const { data: userProvider } = useQuery({
|
||||
queryKey: ["user", "ai-provider"],
|
||||
queryKey: queryKeys.userAiProvider(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/user/ai-provider");
|
||||
if (!res.ok) return null;
|
||||
@@ -208,7 +209,7 @@ function AiInsightsCard({ isAuthenticated }: { isAuthenticated: boolean }) {
|
||||
});
|
||||
|
||||
const { data: chainData } = useQuery({
|
||||
queryKey: ["insights", "provider-chain"],
|
||||
queryKey: queryKeys.insightsProviderChain(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/insights/provider-chain");
|
||||
if (!res.ok) return null;
|
||||
@@ -296,10 +297,10 @@ function AiInsightsCard({ isAuthenticated }: { isAuthenticated: boolean }) {
|
||||
privacyMode={insightsSettings?.privacyMode ?? "aggregated"}
|
||||
lastInsightAt={insightsSettings?.lastInsightAt ?? null}
|
||||
onRegenerated={() =>
|
||||
queryClient.invalidateQueries({ queryKey: ["insights"] })
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.insightsRoot() })
|
||||
}
|
||||
onPrivacyChanged={() =>
|
||||
queryClient.invalidateQueries({ queryKey: ["insights"] })
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.insightsRoot() })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@@ -479,7 +480,7 @@ function CodexProviderForm({
|
||||
url.searchParams.delete("codex_error");
|
||||
window.history.replaceState({}, "", url.toString());
|
||||
if (oauthOutcome.kind === "connected") {
|
||||
queryClient.invalidateQueries({ queryKey: ["insights"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.insightsRoot() });
|
||||
}
|
||||
}, [oauthOutcome, queryClient]);
|
||||
|
||||
@@ -536,7 +537,7 @@ function CodexProviderForm({
|
||||
setDevicePolling(false);
|
||||
setMsg(t("settings.codexConnected"));
|
||||
setMsgType("success");
|
||||
queryClient.invalidateQueries({ queryKey: ["insights"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.insightsRoot() });
|
||||
return;
|
||||
}
|
||||
if (!cancelled) setTimeout(tick, intervalMs);
|
||||
@@ -575,7 +576,7 @@ function CodexProviderForm({
|
||||
}
|
||||
setMsg(t("settings.codexDisconnected"));
|
||||
setMsgType("success");
|
||||
queryClient.invalidateQueries({ queryKey: ["insights"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.insightsRoot() });
|
||||
} catch (err) {
|
||||
setMsg(err instanceof Error ? err.message : t("settings.savingError"));
|
||||
setMsgType("error");
|
||||
@@ -793,8 +794,8 @@ function OpenAIProviderForm({
|
||||
setOk(true);
|
||||
setMsg(t("settings.ai.saved"));
|
||||
setApiKey("");
|
||||
queryClient.invalidateQueries({ queryKey: ["user", "ai-provider"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["insights"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.userAiProvider() });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.insightsRoot() });
|
||||
},
|
||||
onError: (e) => {
|
||||
setOk(false);
|
||||
@@ -979,8 +980,8 @@ function AnthropicProviderForm({
|
||||
setOk(true);
|
||||
setMsg(t("settings.ai.saved"));
|
||||
setApiKey("");
|
||||
queryClient.invalidateQueries({ queryKey: ["user", "ai-provider"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["insights"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.userAiProvider() });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.insightsRoot() });
|
||||
},
|
||||
onError: (e) => {
|
||||
setOk(false);
|
||||
@@ -1135,8 +1136,8 @@ function LocalProviderForm({
|
||||
setOk(true);
|
||||
setMsg(t("settings.ai.saved"));
|
||||
setApiKey("");
|
||||
queryClient.invalidateQueries({ queryKey: ["user", "ai-provider"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["insights"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.userAiProvider() });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.insightsRoot() });
|
||||
},
|
||||
onError: (e) => {
|
||||
setOk(false);
|
||||
@@ -1323,7 +1324,7 @@ function FallbackChainCard({
|
||||
onSuccess: () => {
|
||||
setOk(true);
|
||||
setMsg(t("settings.ai.providerChain.saved"));
|
||||
queryClient.invalidateQueries({ queryKey: ["insights"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.insightsRoot() });
|
||||
},
|
||||
onError: (e) => {
|
||||
setOk(false);
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { formatDate, formatDateTime } from "@/lib/format";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
interface ApiTokenInfo {
|
||||
id: string;
|
||||
@@ -166,7 +167,7 @@ function ApiTokensCard() {
|
||||
const [showRevokedTokens, setShowRevokedTokens] = useState(false);
|
||||
|
||||
const { data: tokens } = useQuery({
|
||||
queryKey: ["tokens"],
|
||||
queryKey: queryKeys.tokens(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/tokens");
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
@@ -191,7 +192,7 @@ function ApiTokensCard() {
|
||||
if (res.ok) {
|
||||
setNewToken(json.data.token);
|
||||
setNewName("");
|
||||
queryClient.invalidateQueries({ queryKey: ["tokens"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.tokens() });
|
||||
} else {
|
||||
setTokenMsg(json.error || t("common.error"));
|
||||
}
|
||||
@@ -205,7 +206,7 @@ function ApiTokensCard() {
|
||||
async function handleRevoke(tokenId: string) {
|
||||
const res = await fetch(`/api/tokens/${tokenId}`, { method: "DELETE" });
|
||||
if (res.ok) {
|
||||
queryClient.invalidateQueries({ queryKey: ["tokens"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.tokens() });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,11 @@ import { IntegrationStatusPill } from "@/components/settings/integration-status-
|
||||
import type { IntegrationPillState } from "@/components/settings/integration-status-pill";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { invalidateKeys, measurementDependentKeys } from "@/lib/query-keys";
|
||||
import {
|
||||
invalidateKeys,
|
||||
measurementDependentKeys,
|
||||
queryKeys,
|
||||
} from "@/lib/query-keys";
|
||||
|
||||
interface MoodLogStatus {
|
||||
configured: boolean;
|
||||
@@ -91,7 +95,7 @@ interface IntegrationStatusEnvelope {
|
||||
*/
|
||||
function useIntegrationStatuses(enabled: boolean) {
|
||||
return useQuery({
|
||||
queryKey: ["integrations", "status"],
|
||||
queryKey: queryKeys.integrationsStatus(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/integrations/status");
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
@@ -155,7 +159,7 @@ export function IntegrationsSection() {
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
const { data: globalServices } = useQuery({
|
||||
queryKey: ["settings", "global-services"],
|
||||
queryKey: queryKeys.settingsGlobalServices(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/settings/global-services");
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
@@ -219,7 +223,7 @@ function WithingsCard({
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: status } = useQuery({
|
||||
queryKey: ["withings", "status"],
|
||||
queryKey: queryKeys.withingsStatus(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/withings/status");
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
@@ -247,8 +251,8 @@ function WithingsCard({
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["withings"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["integrations", "status"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.withings() });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.integrationsStatus() });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -273,7 +277,7 @@ function WithingsCard({
|
||||
);
|
||||
setSyncMsgType("success");
|
||||
void invalidateKeys(queryClient, measurementDependentKeys);
|
||||
queryClient.invalidateQueries({ queryKey: ["integrations", "status"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.integrationsStatus() });
|
||||
} else {
|
||||
setSyncMsg(json.error || t("settings.withingsSyncFailed"));
|
||||
setSyncMsgType("error");
|
||||
@@ -307,7 +311,7 @@ function WithingsCard({
|
||||
setCredsMsgType("success");
|
||||
setClientId("");
|
||||
setClientSecret("");
|
||||
queryClient.invalidateQueries({ queryKey: ["withings"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.withings() });
|
||||
} else {
|
||||
try {
|
||||
const json = await res.json();
|
||||
@@ -601,7 +605,7 @@ function MoodLogCard({
|
||||
const [msgType, setMsgType] = useState<"success" | "error" | null>(null);
|
||||
|
||||
const { data: status, refetch: refetchStatus } = useQuery({
|
||||
queryKey: ["moodlog-status"],
|
||||
queryKey: queryKeys.moodlogStatus(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/integrations/moodlog/status");
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
@@ -624,7 +628,7 @@ function MoodLogCard({
|
||||
setUrl("");
|
||||
setApiKey("");
|
||||
await refetchStatus();
|
||||
queryClient.invalidateQueries({ queryKey: ["integrations", "status"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.integrationsStatus() });
|
||||
} else {
|
||||
const json = await res.json();
|
||||
setMsg(json.error || t("settings.savingError"));
|
||||
@@ -652,7 +656,7 @@ function MoodLogCard({
|
||||
);
|
||||
setMsgType("success");
|
||||
await refetchStatus();
|
||||
queryClient.invalidateQueries({ queryKey: ["integrations", "status"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.integrationsStatus() });
|
||||
} else {
|
||||
setMsg(t("settings.moodLogSyncFailed"));
|
||||
setMsgType("error");
|
||||
@@ -670,7 +674,7 @@ function MoodLogCard({
|
||||
setMsg(t("settings.moodLogDisconnected"));
|
||||
setMsgType("success");
|
||||
await refetchStatus();
|
||||
queryClient.invalidateQueries({ queryKey: ["integrations", "status"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.integrationsStatus() });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { SmilePlus } from "lucide-react";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
interface ProfileShape {
|
||||
moodReminderEnabled: boolean;
|
||||
@@ -25,7 +26,7 @@ export function MoodReminderCard({
|
||||
const [msgType, setMsgType] = useState<"success" | "error" | null>(null);
|
||||
|
||||
const { data: profile } = useQuery({
|
||||
queryKey: ["user", "profile"],
|
||||
queryKey: queryKeys.userProfile(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/user/profile");
|
||||
if (!res.ok) throw new Error("Failed to load profile");
|
||||
@@ -55,7 +56,7 @@ export function MoodReminderCard({
|
||||
: t("notifications.moodReminder.disabledToast"),
|
||||
);
|
||||
setMsgType("success");
|
||||
await queryClient.invalidateQueries({ queryKey: ["user", "profile"] });
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.userProfile() });
|
||||
setOptimistic(null);
|
||||
} else {
|
||||
setOptimistic(null);
|
||||
|
||||
@@ -33,6 +33,7 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
type ChannelType = "TELEGRAM" | "NTFY" | "WEB_PUSH";
|
||||
|
||||
@@ -69,7 +70,7 @@ export function NotificationStatusCard() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["notifications", "status"],
|
||||
queryKey: queryKeys.notificationsStatus(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/notifications/status");
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
@@ -90,7 +91,7 @@ export function NotificationStatusCard() {
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["notifications", "status"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.notificationsStatus() });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -103,7 +104,7 @@ export function NotificationStatusCard() {
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["notifications", "status"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.notificationsStatus() });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ChevronRight } from "lucide-react";
|
||||
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { MoodReminderCard } from "@/components/settings/mood-reminder-card";
|
||||
import { NotificationStatusCard } from "@/components/settings/notification-status-card";
|
||||
import { NtfyCard } from "@/components/settings/ntfy-card";
|
||||
@@ -25,7 +26,7 @@ export function NotificationsSection() {
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
const { data: globalServices } = useQuery({
|
||||
queryKey: ["settings", "global-services"],
|
||||
queryKey: queryKeys.settingsGlobalServices(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/settings/global-services");
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
interface NtfySettings {
|
||||
enabled: boolean;
|
||||
@@ -31,7 +32,7 @@ export function NtfyCard({ isAuthenticated }: { isAuthenticated: boolean }) {
|
||||
);
|
||||
|
||||
const { data: settings } = useQuery({
|
||||
queryKey: ["settings", "ntfy"],
|
||||
queryKey: queryKeys.settingsNtfy(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/settings/ntfy");
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
@@ -70,7 +71,7 @@ export function NtfyCard({ isAuthenticated }: { isAuthenticated: boolean }) {
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["settings", "ntfy"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.settingsNtfy() });
|
||||
setSaveMsg(t("settings.saved"));
|
||||
setSaveMsgType("success");
|
||||
setAuthToken("");
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
interface TelegramSettings {
|
||||
enabled: boolean;
|
||||
@@ -34,7 +35,7 @@ export function TelegramCard({
|
||||
const [msgType, setMsgType] = useState<"success" | "error" | null>(null);
|
||||
|
||||
const { data: settings } = useQuery({
|
||||
queryKey: ["telegram", "settings"],
|
||||
queryKey: queryKeys.telegramSettings(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/settings/telegram");
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
@@ -78,7 +79,7 @@ export function TelegramCard({
|
||||
setMsg(t("settings.telegramSaved"));
|
||||
setMsgType("success");
|
||||
setBotToken("");
|
||||
queryClient.invalidateQueries({ queryKey: ["telegram"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.telegram() });
|
||||
} else {
|
||||
const json = await res.json();
|
||||
setMsg(json.error || t("settings.savingError"));
|
||||
|
||||
@@ -62,7 +62,7 @@ export function ThresholdsEditorSection({ id }: { id: string }) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["user", "thresholds"],
|
||||
queryKey: queryKeys.userThresholds(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/user/thresholds");
|
||||
if (!res.ok) throw new Error("failed");
|
||||
@@ -86,10 +86,10 @@ export function ThresholdsEditorSection({ id }: { id: string }) {
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["user", "thresholds"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.userThresholds() });
|
||||
// Every chart/band depends on these thresholds — invalidate everything.
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.analytics() });
|
||||
queryClient.invalidateQueries({ queryKey: ["insights"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.insightsRoot() });
|
||||
toast.success(t("thresholds.saveSuccess"));
|
||||
},
|
||||
onError: (err) =>
|
||||
@@ -107,9 +107,9 @@ export function ThresholdsEditorSection({ id }: { id: string }) {
|
||||
if (!res.ok) throw new Error("reset failed");
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["user", "thresholds"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.userThresholds() });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.analytics() });
|
||||
queryClient.invalidateQueries({ queryKey: ["insights"] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.insightsRoot() });
|
||||
toast.success(t("thresholds.resetSuccess"));
|
||||
},
|
||||
onError: () => toast.error(t("thresholds.saveError")),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import {
|
||||
DEFAULT_COACH_PREFS,
|
||||
type CoachPrefs,
|
||||
@@ -24,7 +25,7 @@ import {
|
||||
*/
|
||||
export function useCoachPrefs(opts?: { enabled?: boolean }) {
|
||||
return useQuery<CoachPrefs>({
|
||||
queryKey: ["coach-prefs"],
|
||||
queryKey: queryKeys.coachPrefs(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/auth/me/coach-prefs");
|
||||
if (!res.ok) return DEFAULT_COACH_PREFS;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { useContext } from "react";
|
||||
import { QueryClientContext, useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
/**
|
||||
* v1.4.31 — Client-side accessor for the operator's assistant
|
||||
* feature-flag matrix. Mirrors `GET /api/feature-flags`'s response
|
||||
@@ -86,7 +88,7 @@ export function useFeatureFlags(): AssistantFlagSet {
|
||||
|
||||
function useFeatureFlagsQuery(): AssistantFlagSet {
|
||||
const query = useQuery({
|
||||
queryKey: ["feature-flags"],
|
||||
queryKey: queryKeys.featureFlags(),
|
||||
queryFn: fetchFeatureFlags,
|
||||
staleTime: 60_000,
|
||||
gcTime: 300_000,
|
||||
|
||||
@@ -13,7 +13,8 @@ import type { SubPageAnalyticsData } from "@/types/analytics";
|
||||
* `schlaf`) each used to declare:
|
||||
*
|
||||
* - a private `AnalyticsData` interface,
|
||||
* - a `useQuery({ queryKey: ["analytics"], staleTime: 60_000 })` block,
|
||||
* - a `useQuery` block keyed on `["analytics"]` with a 60-second
|
||||
* `staleTime`,
|
||||
* - an `if (isAuthenticated && analytics && !hasMetricData(...)) { … }`
|
||||
* short-circuit that rendered a sub-page-specific empty state.
|
||||
*
|
||||
|
||||
@@ -92,10 +92,12 @@ export function useWorkouts(opts: UseWorkoutsOptions = {}): UseWorkoutsResult {
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [
|
||||
...queryKeys.workoutsRecent(),
|
||||
{ limit: opts.limit, offset: opts.offset, since: opts.since, sportType: opts.sportType },
|
||||
] as const,
|
||||
queryKey: queryKeys.workoutsRecentList({
|
||||
limit: opts.limit,
|
||||
offset: opts.offset,
|
||||
since: opts.since,
|
||||
sportType: opts.sportType,
|
||||
}),
|
||||
queryFn: () => fetchWorkouts(opts),
|
||||
enabled: isAuthenticated,
|
||||
staleTime: 60 * 1000,
|
||||
|
||||
@@ -100,6 +100,74 @@ describe("queryKeys factory", () => {
|
||||
"dashboard-medication-compliance",
|
||||
);
|
||||
});
|
||||
|
||||
// v1.4.42 W3-QUERYKEY-LONGTAIL — pin the shapes of the new factory
|
||||
// entries so a future rename can't drift the cache layout silently.
|
||||
it("exposes the per-medication sub-keys under the medications prefix", () => {
|
||||
expect(queryKeys.medicationCompliance("med1")).toEqual([
|
||||
"medications",
|
||||
"med1",
|
||||
"compliance",
|
||||
]);
|
||||
expect(queryKeys.medicationTitration("med1")).toEqual([
|
||||
"medications",
|
||||
"med1",
|
||||
"titration",
|
||||
]);
|
||||
expect(queryKeys.medicationCadence("med1")).toEqual([
|
||||
"medications",
|
||||
"med1",
|
||||
"cadence",
|
||||
]);
|
||||
expect(queryKeys.medicationGlp1Details("med1")).toEqual([
|
||||
"medications",
|
||||
"med1",
|
||||
"glp1-details",
|
||||
]);
|
||||
expect(queryKeys.medicationIntakeDrugLevelChart("med1")).toEqual([
|
||||
"medications",
|
||||
"med1",
|
||||
"intake",
|
||||
"drug-level-chart",
|
||||
]);
|
||||
const listKey = queryKeys.medicationIntakeList("med1", {
|
||||
sortBy: "takenAt",
|
||||
sortDir: "desc",
|
||||
limit: 25,
|
||||
offset: 0,
|
||||
status: "completed",
|
||||
});
|
||||
expect(listKey[0]).toBe("medications");
|
||||
expect(listKey[1]).toBe("med1");
|
||||
expect(listKey[2]).toBe("intake");
|
||||
expect(listKey[3]).toBe("list");
|
||||
});
|
||||
|
||||
it("exposes withingsStatus under the withings prefix", () => {
|
||||
expect(queryKeys.withingsStatus()).toEqual(["withings", "status"]);
|
||||
});
|
||||
|
||||
it("packs adminAuditLogFiltered params into a stable filtered shape", () => {
|
||||
const key = queryKeys.adminAuditLogFiltered({
|
||||
filter: "all",
|
||||
page: 1,
|
||||
perPage: 50,
|
||||
actor: "",
|
||||
actionFilter: "",
|
||||
target: "",
|
||||
range: "7d",
|
||||
});
|
||||
expect(key[0]).toBe("admin");
|
||||
expect(key[1]).toBe("audit-log");
|
||||
expect(key[2]).toBe("filtered");
|
||||
expect(key.length).toBe(10);
|
||||
});
|
||||
|
||||
it("packs workoutsRecentList opts under the workouts-recent prefix", () => {
|
||||
const key = queryKeys.workoutsRecentList({ limit: 3 });
|
||||
expect(key[0]).toBe("workouts");
|
||||
expect(key[1]).toBe("recent");
|
||||
});
|
||||
});
|
||||
|
||||
describe("dependent-key bundles", () => {
|
||||
@@ -195,11 +263,19 @@ describe("queryKey factory enforcement", () => {
|
||||
join(repoRoot, "src", "app", "page.tsx"),
|
||||
join(repoRoot, "src", "hooks", "use-auth.ts"),
|
||||
// v1.4.41 W-FRONTEND-FACTORY — auth, notifications, and the
|
||||
// about-section migrated to the factory. Future waves extend
|
||||
// this list as they migrate their own surface.
|
||||
// about-section migrated to the factory.
|
||||
join(repoRoot, "src", "app", "auth"),
|
||||
join(repoRoot, "src", "app", "notifications"),
|
||||
join(repoRoot, "src", "components", "settings", "about-section.tsx"),
|
||||
// v1.4.42 W3-QUERYKEY-LONGTAIL — settings / medications /
|
||||
// admin / hooks now route every read through the factory.
|
||||
join(repoRoot, "src", "components", "settings"),
|
||||
join(repoRoot, "src", "components", "medications"),
|
||||
join(repoRoot, "src", "components", "admin"),
|
||||
join(repoRoot, "src", "hooks"),
|
||||
join(repoRoot, "src", "app", "medications", "page.tsx"),
|
||||
join(repoRoot, "src", "app", "medications", "[id]", "history", "page.tsx"),
|
||||
join(repoRoot, "src", "app", "targets", "page.tsx"),
|
||||
];
|
||||
|
||||
function collect(): string[] {
|
||||
|
||||
@@ -73,6 +73,38 @@ export const queryKeys = {
|
||||
medicationDetail: (id: string) => ["medications", id] as const,
|
||||
medicationComplianceChart: (medicationId: string) =>
|
||||
["compliance-chart-inline", medicationId] as const,
|
||||
/**
|
||||
* v1.4.42 W3-QUERYKEY-LONGTAIL — per-medication compliance KPI used
|
||||
* by `medication-card` + `glp1-medication-card`. Centralising lets
|
||||
* the intake mutation invalidate every compliance read through the
|
||||
* `["medications"]` prefix instead of one bare-literal at a time.
|
||||
*/
|
||||
medicationCompliance: (medicationId: string) =>
|
||||
["medications", medicationId, "compliance"] as const,
|
||||
medicationTitration: (medicationId: string) =>
|
||||
["medications", medicationId, "titration"] as const,
|
||||
medicationCadence: (medicationId: string) =>
|
||||
["medications", medicationId, "cadence"] as const,
|
||||
medicationGlp1Details: (medicationId: string) =>
|
||||
["medications", medicationId, "glp1-details"] as const,
|
||||
medicationIntakeDrugLevelChart: (medicationId: string) =>
|
||||
["medications", medicationId, "intake", "drug-level-chart"] as const,
|
||||
/**
|
||||
* v1.4.42 — intake-history list with sort / paging / status filter.
|
||||
* The opaque params object lives at index 4 so the
|
||||
* `["medications", id, "intake", "list"]` prefix invalidates every
|
||||
* sort/page combination on an intake mutation.
|
||||
*/
|
||||
medicationIntakeList: (
|
||||
medicationId: string,
|
||||
params: {
|
||||
sortBy: string;
|
||||
sortDir: string;
|
||||
limit: number;
|
||||
offset: number;
|
||||
status: string;
|
||||
},
|
||||
) => ["medications", medicationId, "intake", "list", params] as const,
|
||||
/**
|
||||
* v1.4.40 W-RSC — the dashboard-level compliance chart (aggregate
|
||||
* across every scheduled medication) was a bare `["medication-
|
||||
@@ -143,11 +175,44 @@ export const queryKeys = {
|
||||
["admin", "host-metrics", window] as const,
|
||||
adminAuditActions: () => ["admin", "audit-log", "actions"] as const,
|
||||
adminAuditOverview: () => ["admin", "audit-log", "overview-preview"] as const,
|
||||
/**
|
||||
* v1.4.42 W3-QUERYKEY-LONGTAIL — paginated + filtered audit-log
|
||||
* read used by the login-overview admin section. The `filtered`
|
||||
* discriminator at index 2 keeps the no-arg `adminAuditLog(filter)`
|
||||
* cache slot byte-distinct so its consumers don't collide.
|
||||
*/
|
||||
adminAuditLogFiltered: (params: {
|
||||
filter: string;
|
||||
page: number;
|
||||
perPage: number;
|
||||
actor: string;
|
||||
actionFilter: string;
|
||||
target: string;
|
||||
range: string;
|
||||
}) =>
|
||||
[
|
||||
"admin",
|
||||
"audit-log",
|
||||
"filtered",
|
||||
params.filter,
|
||||
params.page,
|
||||
params.perPage,
|
||||
params.actor,
|
||||
params.actionFilter,
|
||||
params.target,
|
||||
params.range,
|
||||
] as const,
|
||||
|
||||
tokens: () => ["tokens"] as const,
|
||||
telegram: () => ["telegram"] as const,
|
||||
telegramSettings: () => ["telegram", "settings"] as const,
|
||||
withings: () => ["withings"] as const,
|
||||
/**
|
||||
* v1.4.42 W3-QUERYKEY-LONGTAIL — the per-card Withings status read.
|
||||
* Shares the `["withings"]` prefix with `withings()` so a disconnect
|
||||
* mutation invalidates both at once.
|
||||
*/
|
||||
withingsStatus: () => ["withings", "status"] as const,
|
||||
|
||||
// v1.4.32 — workout list + detail caches. `workouts()` is the
|
||||
// root key invalidated by the batch-ingest mutation; the recent +
|
||||
@@ -155,6 +220,18 @@ export const queryKeys = {
|
||||
// detail page share a cache slot with the list page.
|
||||
workouts: () => ["workouts"] as const,
|
||||
workoutsRecent: () => ["workouts", "recent"] as const,
|
||||
/**
|
||||
* v1.4.42 W3-QUERYKEY-LONGTAIL — the `useWorkouts` hook used to
|
||||
* spread `workoutsRecent()` and append an opts object inline. The
|
||||
* factory now owns the full shape so the hook never reaches for a
|
||||
* literal-array wrapper.
|
||||
*/
|
||||
workoutsRecentList: (opts: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
since?: string;
|
||||
sportType?: string;
|
||||
}) => ["workouts", "recent", opts] as const,
|
||||
workoutDetail: (id: string) => ["workouts", id] as const,
|
||||
|
||||
adminSettings: () => ["admin", "settings"] as const,
|
||||
|
||||
Reference in new issue
Block a user