chore(release): v1.28.44 — loose ends: broken link, API contract, module guards

From the campaign's dead-code + code-health audits (grep-verified; the
custom-metrics query-key finding was verified-false and excluded):

- sleep-overview empty-state CTA linked to /settings/devices, not a valid
  settings slug (dynamicParams=false → 404). Repointed to
  /settings/integrations (where every device OAuth callback already lands).
- six live, iOS-consumed routes were absent from docs/api/openapi.yaml —
  auth/check-user, insights/cards, integrations/healthkit (GET+PATCH),
  measurement-categories, medications/intake/bulk, mood-entries/bulk —
  registered in the Zod OpenAPI registry mirroring each handler's real shape.
- mood/labs/medications module pages lacked the redirect-when-off self-guard
  their siblings have; added, mirroring cycle/achievements exactly.

No migration. Verifier REJECTED the custom-metrics bare-key finding (they are
invalidateQueries prefix filters, not colliding useQuery keys).
This commit is contained in:
Marc-André Bombeck committed 2026-07-16 04:59:27 +02:00
1 parent 8b6024f0d7
commit cd0fbddd1d
17 files changed
+1433 -16

No files matched your search

+13 -3
View File
@@ -49,6 +49,13 @@ export default function LabsPage() {
const [scanOpen, setScanOpen] = useState(false);
const ocrCapability = useOcrCapability(isAuthenticated);
// Gated on the resolved `modules.labs` flag from `GET /api/auth/me` (the
// per-user toggle AND the operator server-wide kill-switch). Default-on: an
// absent key reads as enabled, so a direct URL hit only bounces on an
// explicit `false`. Every `/api/labs/*` route also enforces the gate
// server-side, so this is a UX redirect, not the security boundary.
const enabled = user?.modules?.labs !== false;
const refreshVisible = useCallback(
() => queryClient.invalidateQueries({ type: "active" }),
[queryClient],
@@ -59,12 +66,15 @@ export default function LabsPage() {
});
useEffect(() => {
if (!isLoading && !isAuthenticated) {
if (isLoading) return;
if (!isAuthenticated) {
router.push("/auth/login");
} else if (!enabled) {
router.push("/");
}
}, [isLoading, isAuthenticated, router]);
}, [isLoading, isAuthenticated, enabled, router]);
if (!mounted || isLoading) {
if (!mounted || isLoading || (isAuthenticated && !enabled)) {
return (
<div className="flex h-64 items-center justify-center">
<Loader2 className="text-primary h-8 w-8 animate-spin motion-reduce:animate-none" />
+14 -1
View File
@@ -153,6 +153,12 @@ function MedicationCardSkeleton() {
export default function MedicationsPage() {
const { user, isAuthenticated, isLoading: authLoading } = useAuth();
const { t } = useTranslations();
// v1.18.1 (D3) — medications is an opt-out module. When the account has it
// turned off the whole page disappears (the nav entry is hidden by the same
// gate) and the list query never fires (the API would 403 anyway).
// Default-on: an absent key reads as enabled, so the page only hides on an
// explicit `false`.
const medicationsEnabled = user?.modules?.medications !== false;
const router = useRouter();
const queryClient = useQueryClient();
const searchParams = useSearchParams();
@@ -195,7 +201,7 @@ export default function MedicationsPage() {
queryFn: async () => {
return apiGet<Medication[]>("/api/medications");
},
enabled: isAuthenticated,
enabled: isAuthenticated && medicationsEnabled,
// v1.16.12 (#316) — refetch on every mount, even inside the global
// staleTime window. App Router unmounts this page on navigation away
// and remounts it on return; without this, a return within 5 min
@@ -277,6 +283,13 @@ export default function MedicationsPage() {
);
}
// Module off ⇒ the surface disappears entirely. The nav entry is hidden by
// the same gate, so a direct hit renders nothing rather than an orphaned
// shell.
if (isAuthenticated && !medicationsEnabled) {
return null;
}
if (!isAuthenticated) {
return (
<div className="space-y-6">
+14 -4
View File
@@ -19,7 +19,7 @@ import { useEffect } from "react";
import { useTranslations } from "@/lib/i18n/context";
export default function MoodPage() {
const { isAuthenticated, isLoading } = useAuth();
const { user, isAuthenticated, isLoading } = useAuth();
const mounted = useMounted();
const router = useRouter();
const queryClient = useQueryClient();
@@ -30,6 +30,13 @@ export default function MoodPage() {
const [footerEl, setFooterEl] = useState<HTMLDivElement | null>(null);
const { t } = useTranslations();
// Gated on the resolved `modules.mood` flag from `GET /api/auth/me` (the
// per-user toggle AND the operator server-wide kill-switch). Default-on: an
// absent key reads as enabled, so a direct URL hit only bounces on an
// explicit `false`. Every `/api/mood-entries/*` route also enforces the gate
// server-side, so this is a UX redirect, not the security boundary.
const enabled = user?.modules?.mood !== false;
// v1.16.4 — PWA pull-to-refresh: a top-anchored touch pull refetches
// whatever this page currently has mounted (`type: "active"` scopes the
// invalidation to visible queries). Suspended while the add-sheet is
@@ -44,17 +51,20 @@ export default function MoodPage() {
});
useEffect(() => {
if (!isLoading && !isAuthenticated) {
if (isLoading) return;
if (!isAuthenticated) {
router.push("/auth/login");
} else if (!enabled) {
router.push("/");
}
}, [isLoading, isAuthenticated, router]);
}, [isLoading, isAuthenticated, enabled, router]);
// `!mounted` keeps the hydration render identical to the SSR HTML: the
// auth query is fired by the early-hydrating shell and can settle before
// this page boundary hydrates, so `isLoading` alone flipped the branch
// and React logged hydration error #418 — same fix as the measurements
// page; see `useMounted`.
if (!mounted || isLoading) {
if (!mounted || isLoading || (isAuthenticated && !enabled)) {
return <PageAuthGate label={t("common.loading")} />;
}