mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +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
+2
-1
@@ -5127,7 +5127,8 @@
|
||||
"description": "Wie lange zeitgebundene Datensätze vor der automatischen Bereinigung gespeichert bleiben.",
|
||||
"coach": "Coach-Unterhaltungen: {days} Tage",
|
||||
"audit": "Sicherheitsprotokoll: {days} Tage",
|
||||
"delivery": "Benachrichtigungsprotokoll: {days} Tage"
|
||||
"delivery": "Benachrichtigungsprotokoll: {days} Tage",
|
||||
"unavailable": "Aufbewahrungsdetails sind derzeit nicht verfügbar."
|
||||
},
|
||||
"export": {
|
||||
"title": "Meine Daten herunterladen",
|
||||
|
||||
+2
-1
@@ -5127,7 +5127,8 @@
|
||||
"description": "How long time-bound records are kept before automatic cleanup.",
|
||||
"coach": "Coach conversations: kept {days} days",
|
||||
"audit": "Security activity log: kept {days} days",
|
||||
"delivery": "Notification delivery log: kept {days} days"
|
||||
"delivery": "Notification delivery log: kept {days} days",
|
||||
"unavailable": "Retention details are unavailable right now."
|
||||
},
|
||||
"export": {
|
||||
"title": "Download my data",
|
||||
|
||||
+2
-1
@@ -5127,7 +5127,8 @@
|
||||
"description": "Cuánto tiempo se conservan los registros con límite temporal antes de su limpieza automática.",
|
||||
"coach": "Conversaciones con el asistente: {days} días",
|
||||
"audit": "Registro de seguridad: {days} días",
|
||||
"delivery": "Registro de envío de notificaciones: {days} días"
|
||||
"delivery": "Registro de envío de notificaciones: {days} días",
|
||||
"unavailable": "Los detalles de retención no están disponibles en este momento."
|
||||
},
|
||||
"export": {
|
||||
"title": "Descargar mis datos",
|
||||
|
||||
+2
-1
@@ -5127,7 +5127,8 @@
|
||||
"description": "Durée de conservation des enregistrements limités dans le temps avant le nettoyage automatique.",
|
||||
"coach": "Conversations avec le coach : {days} jours",
|
||||
"audit": "Journal de sécurité : {days} jours",
|
||||
"delivery": "Journal d'envoi des notifications : {days} jours"
|
||||
"delivery": "Journal d'envoi des notifications : {days} jours",
|
||||
"unavailable": "Les détails de conservation sont indisponibles pour le moment."
|
||||
},
|
||||
"export": {
|
||||
"title": "Télécharger mes données",
|
||||
|
||||
+2
-1
@@ -5127,7 +5127,8 @@
|
||||
"description": "Per quanto tempo i record a tempo vengono conservati prima della pulizia automatica.",
|
||||
"coach": "Conversazioni con il coach: {days} giorni",
|
||||
"audit": "Registro di sicurezza: {days} giorni",
|
||||
"delivery": "Registro di invio delle notifiche: {days} giorni"
|
||||
"delivery": "Registro di invio delle notifiche: {days} giorni",
|
||||
"unavailable": "I dettagli di conservazione non sono disponibili al momento."
|
||||
},
|
||||
"export": {
|
||||
"title": "Scarica i miei dati",
|
||||
|
||||
+2
-1
@@ -5127,7 +5127,8 @@
|
||||
"description": "Jak długo rekordy ograniczone czasowo są przechowywane przed automatycznym czyszczeniem.",
|
||||
"coach": "Rozmowy z asystentem: {days} dni",
|
||||
"audit": "Dziennik zabezpieczeń: {days} dni",
|
||||
"delivery": "Dziennik wysyłki powiadomień: {days} dni"
|
||||
"delivery": "Dziennik wysyłki powiadomień: {days} dni",
|
||||
"unavailable": "Szczegóły przechowywania są obecnie niedostępne."
|
||||
},
|
||||
"export": {
|
||||
"title": "Pobierz moje dane",
|
||||
|
||||
@@ -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