mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
refactor(query-keys): expand factory and migrate auth, notifications, about
Routes the registration-status, auth invalidation, notifications preferences, and Settings about-section api-version reads through the centralised queryKeys factory. Adds factory entries for the remaining settings/admin surfaces a follow-up will migrate, and extends the walker test to fail on any future bare-literal queryKey in src/app/auth, src/app/notifications, and the about-section. Also swaps the dashboard tile Suspense fallback from null to a layout-stable card placeholder so a future RSC hoist of any tile slot can't trigger CLS.
This commit is contained in:
1 parent
59befdca73
commit
0bf07abd9d
7 files changed
+106
-14
No files matched your search
@@ -85,7 +85,7 @@ export default function LoginPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
await queryClient.invalidateQueries({ queryKey: ["auth"] });
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.auth() });
|
||||
router.push(getRedirectTarget());
|
||||
} catch (err) {
|
||||
const { key, params } = describePasskeyError(err);
|
||||
@@ -114,7 +114,7 @@ export default function LoginPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
await queryClient.invalidateQueries({ queryKey: ["auth"] });
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.auth() });
|
||||
router.push(getRedirectTarget());
|
||||
} catch {
|
||||
setError(t("auth.loginFailed"));
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Label } from "@/components/ui/label";
|
||||
import { PasswordStrength } from "@/components/ui/password-strength";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { detectBrowserTimezone } from "@/lib/tz/format";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
export default function RegisterPage() {
|
||||
const router = useRouter();
|
||||
@@ -54,7 +55,7 @@ export default function RegisterPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
await queryClient.invalidateQueries({ queryKey: ["auth"] });
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.auth() });
|
||||
router.push("/");
|
||||
} catch {
|
||||
setError(t("auth.registerFailed"));
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
EVENT_DEFAULT_ENABLED,
|
||||
type EventType,
|
||||
} from "@/lib/notifications/types";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
interface ChannelInfo {
|
||||
id: string;
|
||||
@@ -86,7 +87,7 @@ export default function NotificationsPage() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ["notifications", "preferences"],
|
||||
queryKey: queryKeys.notificationsPreferences(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/notifications/preferences");
|
||||
if (!res.ok) throw new Error("Failed to load preferences");
|
||||
@@ -112,12 +113,11 @@ export default function NotificationsPage() {
|
||||
},
|
||||
onMutate: async ({ channelId, eventType, enabled }) => {
|
||||
await queryClient.cancelQueries({
|
||||
queryKey: ["notifications", "preferences"],
|
||||
queryKey: queryKeys.notificationsPreferences(),
|
||||
});
|
||||
const prev = queryClient.getQueryData<PreferencesData>([
|
||||
"notifications",
|
||||
"preferences",
|
||||
]);
|
||||
const prev = queryClient.getQueryData<PreferencesData>(
|
||||
queryKeys.notificationsPreferences(),
|
||||
);
|
||||
if (prev) {
|
||||
const existing = prev.preferences.find(
|
||||
(p) => p.channelId === channelId && p.eventType === eventType,
|
||||
@@ -130,7 +130,7 @@ export default function NotificationsPage() {
|
||||
)
|
||||
: [...prev.preferences, { channelId, eventType, enabled }];
|
||||
queryClient.setQueryData<PreferencesData>(
|
||||
["notifications", "preferences"],
|
||||
queryKeys.notificationsPreferences(),
|
||||
{ ...prev, preferences: newPreferences },
|
||||
);
|
||||
}
|
||||
@@ -139,14 +139,14 @@ export default function NotificationsPage() {
|
||||
onError: (_err, _vars, context) => {
|
||||
if (context?.prev) {
|
||||
queryClient.setQueryData(
|
||||
["notifications", "preferences"],
|
||||
queryKeys.notificationsPreferences(),
|
||||
context.prev,
|
||||
);
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["notifications", "preferences"],
|
||||
queryKey: queryKeys.notificationsPreferences(),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
+19
-1
@@ -1413,7 +1413,25 @@ export default function DashboardPage() {
|
||||
* future RSC hoists of any tile slot won't need a
|
||||
* second pass to add the fallback infrastructure.
|
||||
*/}
|
||||
<Suspense fallback={null}>{entry.node}</Suspense>
|
||||
{/*
|
||||
* v1.4.41 W-FRONTEND-FACTORY (UX M1) — swap the
|
||||
* `null` fallback for a layout-stable placeholder
|
||||
* that mirrors the trend-card chrome. The body
|
||||
* stays synchronous today so the fallback rarely
|
||||
* paints, but a future RSC hoist of any tile slot
|
||||
* would otherwise leave the grid track empty and
|
||||
* trigger CLS as the cell paints in.
|
||||
*/}
|
||||
<Suspense
|
||||
fallback={
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="bg-card border-border h-full w-full rounded-xl border p-4 md:p-6"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{entry.node}
|
||||
</Suspense>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
interface VersionPayload {
|
||||
version: string;
|
||||
@@ -117,7 +118,7 @@ export function AboutSection() {
|
||||
const fmt = useFormatters();
|
||||
|
||||
const { data: version, isLoading } = useQuery({
|
||||
queryKey: ["api", "version"],
|
||||
queryKey: queryKeys.apiVersion(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/version");
|
||||
if (!res.ok) throw new Error("version-fetch-failed");
|
||||
|
||||
@@ -49,6 +49,25 @@ describe("queryKeys factory", () => {
|
||||
expect(queryKeys.userThresholds()).toEqual(["user", "thresholds"]);
|
||||
});
|
||||
|
||||
// v1.4.41 W-FRONTEND-FACTORY — pin the shapes that auth/login + the
|
||||
// notifications surface migrated to so a future rename can't drift
|
||||
// the cache layout silently.
|
||||
it("exposes auth + notifications keys for the factory-migrated surfaces", () => {
|
||||
expect(queryKeys.authRegistrationStatus()).toEqual([
|
||||
"auth",
|
||||
"registration-status",
|
||||
]);
|
||||
expect(queryKeys.notificationsPreferences()).toEqual([
|
||||
"notifications",
|
||||
"preferences",
|
||||
]);
|
||||
expect(queryKeys.notificationsStatus()).toEqual([
|
||||
"notifications",
|
||||
"status",
|
||||
]);
|
||||
expect(queryKeys.apiVersion()).toEqual(["api", "version"]);
|
||||
});
|
||||
|
||||
it("packs chartData params into a stable tuple prefixed by chart-data", () => {
|
||||
const key = queryKeys.chartData(
|
||||
"WEIGHT",
|
||||
@@ -175,6 +194,12 @@ describe("queryKey factory enforcement", () => {
|
||||
join(repoRoot, "src", "components", "comparison"),
|
||||
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.
|
||||
join(repoRoot, "src", "app", "auth"),
|
||||
join(repoRoot, "src", "app", "notifications"),
|
||||
join(repoRoot, "src", "components", "settings", "about-section.tsx"),
|
||||
];
|
||||
|
||||
function collect(): string[] {
|
||||
|
||||
@@ -93,9 +93,56 @@ export const queryKeys = {
|
||||
passkeys: () => ["passkeys"] as const,
|
||||
|
||||
notificationsPreferences: () => ["notifications", "preferences"] as const,
|
||||
notificationsStatus: () => ["notifications", "status"] as const,
|
||||
|
||||
settingsGlobalServices: () => ["settings", "global-services"] as const,
|
||||
settingsNtfy: () => ["settings", "ntfy"] as const,
|
||||
settingsReminderThresholds: () =>
|
||||
["settings", "reminder-thresholds"] as const,
|
||||
|
||||
/**
|
||||
* v1.4.41 W-FRONTEND-FACTORY — Settings → AI surfaces (provider chain,
|
||||
* insights settings, user provider preference) and the targets editor
|
||||
* all read these endpoints; centralising the keys keeps invalidation
|
||||
* symmetrical with the user-thresholds + auth surfaces.
|
||||
*/
|
||||
insightsSettings: () => ["insights", "settings"] as const,
|
||||
insightsProviderChain: () => ["insights", "provider-chain"] as const,
|
||||
insightsGlp1Timeline: (limit: number | string) =>
|
||||
["insights", "glp1-timeline", limit] as const,
|
||||
userAiProvider: () => ["user", "ai-provider"] as const,
|
||||
userProfile: () => ["user", "profile"] as const,
|
||||
|
||||
apiVersion: () => ["api", "version"] as const,
|
||||
publicVersion: () => ["public", "version"] as const,
|
||||
researchMode: () => ["research-mode"] as const,
|
||||
moodlogStatus: () => ["moodlog-status"] as const,
|
||||
integrationsStatus: () => ["integrations", "status"] as const,
|
||||
featureFlags: () => ["feature-flags"] as const,
|
||||
coachPrefs: () => ["coach-prefs"] as const,
|
||||
|
||||
/**
|
||||
* v1.4.41 — admin surfaces. Pre-fix every admin section declared its
|
||||
* own bare-literal `["admin", "<name>"]`. Routing through the factory
|
||||
* lets a single rename change every consumer in lockstep.
|
||||
*/
|
||||
adminAiQuality: () => ["admin", "ai-quality"] as const,
|
||||
adminAppLogs: (
|
||||
traceId: string | undefined,
|
||||
action: string | undefined,
|
||||
level: string | undefined,
|
||||
range: string | undefined,
|
||||
) =>
|
||||
["admin", "app-logs", traceId, action, level, range] as const,
|
||||
adminAssistantFlags: () => ["admin", "settings", "assistant-flags"] as const,
|
||||
adminBackups: () => ["admin", "backups"] as const,
|
||||
adminCoachFeedback: () => ["admin", "coach-feedback"] as const,
|
||||
adminFeedback: (status: string) => ["admin", "feedback", status] as const,
|
||||
adminFeedbackRoot: () => ["admin", "feedback"] as const,
|
||||
adminHostMetrics: (window: string) =>
|
||||
["admin", "host-metrics", window] as const,
|
||||
adminAuditActions: () => ["admin", "audit-log", "actions"] as const,
|
||||
adminAuditOverview: () => ["admin", "audit-log", "overview-preview"] as const,
|
||||
|
||||
tokens: () => ["tokens"] as const,
|
||||
telegram: () => ["telegram"] as const,
|
||||
|
||||
Reference in new issue
Block a user