chore(release): v1.4.42 — Knip enforcing, queryKey factory closed, iOS Workouts dedup, Withings off-response classification

Six touch-disjoint waves landed on develop before this release:

- W1-KNIP — exports/types tiers flipped to enforcing-mode on main;
  35 unused exports + 52 unused types triaged to 0/0; shadcn surface
  + zod-infer contract types scoped via knip.json ignore-block.
- W2-ZOD-MULTI-ISSUE — `returnAllZodIssues` shared helper +
  `/api/dashboard/widgets` 422 now returns every Zod issue + worker
  audit-ledger breadcrumb. 41 sibling routes documented for v1.4.43.
- W3-QUERYKEY-LONGTAIL — 40 settings/medications/admin/hooks files
  migrated to the queryKeys factory; 9 new factory entries; ESLint
  rule + test-guard scope extended in lockstep.
- W4-ARCH-HYGIENE — BERLIN_DAY_FORMATTER consolidated across nine
  sites (seven status helpers + bucket-series + bp-in-target);
  Suspense double-comment consolidated; doctor-report sanitiser regex
  rewritten to escape-form so diffs are reviewable; pr-detection-
  worker soft-delete filter; offhost-backup DR-intent comment.
- W5-IOS-WORKOUTS — `dedupeWorkoutBatch` write-time cross-source
  dedup helper for the v1.5 iOS HealthKit observer ingest; dashboard
  tile-strip placeholder parity with the live TrendCard chrome.
- W6-WITHINGS-OFF + ENV-CHECK — Withings off-response classifier
  (transient/reauth/persistent) + WithingsApiError typed throw + the
  `pnpm check-env` CLI. APNs `allOrNone: true` closes the AP-2
  silent-disable gap the wave was conceived to close.

No migration, no env-var change, no API contract break for iOS v0.5.4.
The new `/api/dashboard/widgets` 422 envelope is additively extended.

pnpm test --run green at 4815 passing / 1 skipped.
pnpm typecheck, pnpm lint, pnpm knip (no --include flag) green.
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-05-21 19:33:07 +02:00
1 parent 67207d725e
commit 9b6e237f5f
121 files changed
+6481 -590

No files matched your search

+4 -3
View File
@@ -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");
+2 -1
View File
@@ -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 -4
View File
@@ -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(),
});
},
});
+5 -4
View File
@@ -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 (
+2 -1
View File
@@ -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"));
},