mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +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
@@ -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")),
|
||||
|
||||
Reference in new issue
Block a user