mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
fix(privacy): de-duplicate trusted devices and handle the summary loading state
Trusted devices was mounted on both the Security section and the Privacy dashboard. Keep it only on the Privacy dashboard, which already gathers sessions and security activity, and drop it from the Security section. While here: the Retention card painted an empty list while the privacy summary was loading and on error — give it a skeleton placeholder and a one-line fallback, and align the "What's stored" indentation with the other privacy cards.
This commit is contained in:
1 parent
4a1f7f93df
commit
361505bd31
9 files changed
+85
-33
No files matched your search
@@ -33,7 +33,17 @@ function render(node: React.ReactNode, locale: "en" | "de" = "en") {
|
||||
// assembled cards paint without firing network.
|
||||
client.setQueryData(queryKeys.sessions(), { sessions: [] });
|
||||
client.setQueryData(queryKeys.securityActivity(), { events: [] });
|
||||
client.setQueryData(queryKeys.trustedDevices(), { devices: [] });
|
||||
// Seed a trusted device so the card paints (it hides itself when empty).
|
||||
client.setQueryData(queryKeys.trustedDevices(), {
|
||||
devices: [
|
||||
{
|
||||
id: "td-1",
|
||||
label: "Firefox on macOS",
|
||||
isCurrent: true,
|
||||
expiresAt: "2099-01-01T00:00:00.000Z",
|
||||
},
|
||||
],
|
||||
});
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale={locale}>
|
||||
<QueryClientProvider client={client}>{node}</QueryClientProvider>
|
||||
@@ -56,5 +66,31 @@ describe("<PrivacySection> — assembled Data & Privacy dashboard", () => {
|
||||
expect(html).toContain('href="/settings/advanced"');
|
||||
// Embedded active-session + security-activity cards.
|
||||
expect(html).toContain("settings-security-sessions-card");
|
||||
// Trusted devices now lives here (and only here) — its single home is the
|
||||
// Privacy dashboard, not the Security section.
|
||||
expect(html).toContain("settings-trusted-devices-card");
|
||||
});
|
||||
|
||||
it("shows a loading placeholder for the retention block before the summary resolves", () => {
|
||||
// Render WITHOUT seeding the privacy summary: the query is pending, so the
|
||||
// retention block must paint a skeleton rather than an empty list.
|
||||
const client = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, refetchOnMount: false },
|
||||
},
|
||||
});
|
||||
client.setQueryData(queryKeys.sessions(), { sessions: [] });
|
||||
client.setQueryData(queryKeys.securityActivity(), { events: [] });
|
||||
client.setQueryData(queryKeys.trustedDevices(), { devices: [] });
|
||||
const html = renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">
|
||||
<QueryClientProvider client={client}>
|
||||
<PrivacySection />
|
||||
</QueryClientProvider>
|
||||
</I18nProvider>,
|
||||
);
|
||||
// Skeleton rows render while the summary is loading; no retention day-count.
|
||||
expect(html).toContain('data-slot="skeleton"');
|
||||
expect(html).not.toContain("365");
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { SettingsInfoTile } from "@/components/settings/_info-tile";
|
||||
@@ -53,7 +54,11 @@ export function PrivacySection() {
|
||||
const { t } = useTranslations();
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
const { data: summary } = useQuery({
|
||||
const {
|
||||
data: summary,
|
||||
isLoading: summaryLoading,
|
||||
isError: summaryError,
|
||||
} = useQuery({
|
||||
queryKey: queryKeys.privacySummary(),
|
||||
queryFn: () => apiGet<PrivacySummary>("/api/settings/privacy-summary"),
|
||||
enabled: isAuthenticated,
|
||||
@@ -99,7 +104,7 @@ export function PrivacySection() {
|
||||
description={t("settings.privacy.stored.description")}
|
||||
className="mb-4"
|
||||
/>
|
||||
<ul className="text-muted-foreground list-disc space-y-1 pl-12 text-sm">
|
||||
<ul className="text-muted-foreground list-disc space-y-1 pl-7 text-sm">
|
||||
<li>{t("settings.privacy.stored.measurements")}</li>
|
||||
<li>{t("settings.privacy.stored.medications")}</li>
|
||||
<li>{t("settings.privacy.stored.moodLabs")}</li>
|
||||
@@ -117,27 +122,35 @@ export function PrivacySection() {
|
||||
description={t("settings.privacy.retention.description")}
|
||||
className="mb-4"
|
||||
/>
|
||||
<ul className="text-muted-foreground space-y-1 pl-7 text-sm">
|
||||
{summary && (
|
||||
<>
|
||||
<li>
|
||||
{t("settings.privacy.retention.coach", {
|
||||
days: summary.retention.coachMessagesDays,
|
||||
})}
|
||||
</li>
|
||||
<li>
|
||||
{t("settings.privacy.retention.audit", {
|
||||
days: summary.retention.auditLogDays,
|
||||
})}
|
||||
</li>
|
||||
<li>
|
||||
{t("settings.privacy.retention.delivery", {
|
||||
days: summary.retention.deliveryLogDays,
|
||||
})}
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
{summaryLoading ? (
|
||||
<div className="space-y-2 pl-7">
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-4 w-2/3" />
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
</div>
|
||||
) : summaryError || !summary ? (
|
||||
<p className="text-muted-foreground pl-7 text-sm">
|
||||
{t("settings.privacy.retention.unavailable")}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="text-muted-foreground space-y-1 pl-7 text-sm">
|
||||
<li>
|
||||
{t("settings.privacy.retention.coach", {
|
||||
days: summary.retention.coachMessagesDays,
|
||||
})}
|
||||
</li>
|
||||
<li>
|
||||
{t("settings.privacy.retention.audit", {
|
||||
days: summary.retention.auditLogDays,
|
||||
})}
|
||||
</li>
|
||||
<li>
|
||||
{t("settings.privacy.retention.delivery", {
|
||||
days: summary.retention.deliveryLogDays,
|
||||
})}
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
</SettingsCard>
|
||||
|
||||
{/* Export */}
|
||||
|
||||
@@ -10,7 +10,6 @@ import { TotpCard } from "./totp-card";
|
||||
import { SecurityKeysCard, type WebauthnKeyInfo } from "./security-keys-card";
|
||||
import { PasskeyListSection } from "./passkey-list-section";
|
||||
import { PasskeyUpgradeNudge } from "./passkey-upgrade-nudge";
|
||||
import { TrustedDevicesCard } from "@/components/settings/trusted-devices-card";
|
||||
|
||||
interface MfaStatus {
|
||||
totp: { enabled: boolean };
|
||||
@@ -68,8 +67,6 @@ export function SecuritySection() {
|
||||
<SecurityKeysCard keys={status.webauthn} />
|
||||
|
||||
<PasskeyListSection isAuthenticated={isAuthenticated} />
|
||||
|
||||
<TrustedDevicesCard isAuthenticated={isAuthenticated} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user