mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
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:
1 parent
67207d725e
commit
9b6e237f5f
121 files changed
+6481
-590
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"));
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user