release: v1.32.0 — hardening and scalability (#580)

* release: prepare v1.32.0

* test: stabilize water capture re-entry
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-07-22 04:14:15 +02:00
1 parent b01822be48
commit 62eb431d66
372 files changed
+25178 -8561

No files matched your search

@@ -0,0 +1,155 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import type { Locale } from "@/lib/i18n/config";
import { I18nProvider } from "@/lib/i18n/context";
import de from "../../../../../messages/de.json";
import en from "../../../../../messages/en.json";
import es from "../../../../../messages/es.json";
import fr from "../../../../../messages/fr.json";
import itMessages from "../../../../../messages/it.json";
import pl from "../../../../../messages/pl.json";
let statusPayload: { lastSyncedAt: string | null } | undefined;
let statusLoading = false;
let statusError = false;
vi.mock("@tanstack/react-query", () => ({
useQuery: () => ({
data: statusPayload,
isLoading: statusLoading,
isError: statusError,
}),
}));
import { AppleHealthCard } from "../apple-health-card";
function render(locale: Locale = "en") {
return renderToStaticMarkup(
<I18nProvider initialLocale={locale}>
<AppleHealthCard enabled />
</I18nProvider>,
);
}
describe("<AppleHealthCard>", () => {
beforeEach(() => {
statusPayload = undefined;
statusLoading = false;
statusError = false;
});
it("explains that live sync uses the iOS app rather than OAuth", () => {
statusPayload = { lastSyncedAt: null };
statusLoading = false;
const html = render();
expect(html).toContain('data-testid="apple-health-card"');
expect(html).toContain("HealthLog iOS app");
expect(html).toContain("not a web or OAuth connection");
expect(html).toContain("Settings → Apple Health");
expect(html).toContain("when iOS grants background time");
expect(html).not.toContain(">Connected<");
});
it("keeps the status neutral when no Apple Health data has been observed", () => {
statusPayload = { lastSyncedAt: null };
statusLoading = false;
const html = render();
expect(html).toContain('data-testid="apple-health-status"');
expect(html).toContain('data-state="setup"');
expect(html).toContain("Set up in iOS app");
expect(html).not.toContain('data-state="connected"');
});
it("reports only observed data freshness without claiming a connection", () => {
statusPayload = { lastSyncedAt: "2026-07-20T10:30:00.000Z" };
statusLoading = false;
const html = render();
expect(html).toContain('data-state="recent-data"');
expect(html).toContain("Apple Health data received");
expect(html).toContain("Last Apple Health data:");
expect(html).toContain('dateTime="2026-07-20T10:30:00.000Z"');
expect(html).not.toContain(">Connected<");
expect(html).not.toContain('data-state="connected"');
});
it("links to the existing one-shot Apple Health import fallback", () => {
statusPayload = { lastSyncedAt: null };
statusLoading = false;
const html = render();
expect(html).toContain('data-testid="apple-health-import-link"');
expect(html).toContain(
'href="/settings/export#settings-section-import-title"',
);
expect(html).toContain("Open one-shot import");
});
it("uses a non-committal checking state while status is loading", () => {
statusPayload = undefined;
statusLoading = true;
const html = render();
expect(html).toContain('data-state="checking"');
expect(html).toContain("Checking Apple Health data…");
expect(html).not.toContain("Connected");
});
it("does not turn a failed status read into a setup or connection claim", () => {
statusError = true;
const html = render();
expect(html).toContain('data-state="unavailable"');
expect(html).toContain("Apple Health status unavailable");
expect(html).not.toContain('data-state="setup"');
expect(html).not.toContain("Connected");
});
it("defines every card key in all six locale catalogs", () => {
const requiredKeys = [
"title",
"description",
"lastDataLabel",
"setupTitle",
"permissionStep",
"backgroundStep",
"importNote",
"importAction",
] as const;
for (const catalog of [en, de, es, fr, itMessages, pl]) {
const appleHealth = catalog.settings.appleHealth;
for (const key of requiredKeys) {
expect(appleHealth[key]).toBeTypeOf("string");
expect(appleHealth[key].length).toBeGreaterThan(0);
}
expect(Object.keys(appleHealth.status).sort()).toEqual([
"checking",
"dataReceived",
"setup",
"unavailable",
]);
}
});
it.each(["en", "de", "es", "fr", "it", "pl"] as const)(
"renders localized Apple Health copy for %s without leaking keys",
(locale) => {
statusPayload = { lastSyncedAt: null };
statusLoading = false;
const html = render(locale);
expect(html).toContain("Apple Health");
expect(html).not.toContain("settings.appleHealth.");
},
);
});
@@ -0,0 +1,123 @@
"use client";
import Link from "next/link";
import { useQuery } from "@tanstack/react-query";
import { CheckCircle2, HeartPulse, Info, Upload } from "lucide-react";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { SettingsCard } from "@/components/settings/settings-card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { apiGet } from "@/lib/api/api-fetch";
import { useFormatters, useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
interface HealthKitStatus {
lastSyncedAt: string | null;
}
export function AppleHealthCard({ enabled }: { enabled: boolean }) {
const { t } = useTranslations();
const format = useFormatters();
const {
data: status,
isLoading,
isError,
} = useQuery({
queryKey: queryKeys.healthKitStatus(),
queryFn: () => apiGet<HealthKitStatus>("/api/integrations/healthkit"),
enabled,
refetchOnWindowFocus: true,
});
const lastSyncedAt = status?.lastSyncedAt ?? null;
const statusState = isLoading
? "checking"
: isError
? "unavailable"
: lastSyncedAt
? "recent-data"
: "setup";
const statusLabel = isLoading
? t("settings.appleHealth.status.checking")
: isError
? t("settings.appleHealth.status.unavailable")
: lastSyncedAt
? t("settings.appleHealth.status.dataReceived")
: t("settings.appleHealth.status.setup");
return (
<SettingsCard
as="section"
aria-labelledby="apple-health-card-title"
data-testid="apple-health-card"
className="space-y-4"
>
<SettingsCardHeader
icon={HeartPulse}
titleId="apple-health-card-title"
title={t("settings.appleHealth.title")}
description={t("settings.appleHealth.description")}
status={
<Badge
variant={lastSyncedAt ? undefined : "outline"}
data-testid="apple-health-status"
data-state={statusState}
className={
lastSyncedAt
? "border-success/30 bg-success/15 text-success max-w-full whitespace-nowrap"
: "max-w-full whitespace-nowrap"
}
>
{lastSyncedAt ? (
<CheckCircle2 className="h-3 w-3" aria-hidden="true" />
) : (
<Info className="h-3 w-3" aria-hidden="true" />
)}
<span className="truncate">{statusLabel}</span>
</Badge>
}
/>
{lastSyncedAt && (
<p
className="text-muted-foreground text-xs"
data-testid="apple-health-last-data"
>
{t("settings.appleHealth.lastDataLabel")}{" "}
<time dateTime={lastSyncedAt}>{format.dateTime(lastSyncedAt)}</time>
</p>
)}
<div className="space-y-2">
<h3 className="text-sm font-semibold">
{t("settings.appleHealth.setupTitle")}
</h3>
<ol className="text-muted-foreground list-decimal space-y-2 pl-5 text-sm">
<li>{t("settings.appleHealth.permissionStep")}</li>
<li>{t("settings.appleHealth.backgroundStep")}</li>
</ol>
</div>
<div className="border-border bg-muted/30 flex flex-col gap-3 rounded-lg border p-3 sm:flex-row sm:items-center sm:justify-between">
<p className="text-muted-foreground text-xs sm:max-w-xl">
{t("settings.appleHealth.importNote")}
</p>
<Button
asChild
variant="outline"
size="sm"
className="min-h-11 shrink-0 sm:min-h-9"
>
<Link
href="/settings/export#settings-section-import-title"
data-testid="apple-health-import-link"
>
<Upload className="h-3.5 w-3.5" aria-hidden="true" />
{t("settings.appleHealth.importAction")}
</Link>
</Button>
</div>
</SettingsCard>
);
}
@@ -16,6 +16,7 @@ import { useRouter } from "next/navigation";
import { useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { AppleHealthCard } from "@/components/settings/integrations/apple-health-card";
import { FitbitCard } from "@/components/settings/integrations/fitbit-card";
import { GarminInfoNote } from "@/components/settings/integrations/garmin-info-note";
import { GoogleHealthCard } from "@/components/settings/integrations/google-health-card";
@@ -285,6 +286,9 @@ export function ConnectionsPanel() {
<div id="google-health" className="scroll-mt-28">
<GoogleHealthCard viewModel={googleHealthViewModel} />
</div>
<div id="apple-health" className="scroll-mt-28">
<AppleHealthCard enabled={isAuthenticated} />
</div>
<div id="polar" className="scroll-mt-28">
<PolarCard enabled={isAuthenticated} viewModel={polarViewModel} />
</div>