diff --git a/messages/de.json b/messages/de.json index 628c3fd9f..62913d6c2 100644 --- a/messages/de.json +++ b/messages/de.json @@ -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", diff --git a/messages/en.json b/messages/en.json index 79eddff16..761a1d876 100644 --- a/messages/en.json +++ b/messages/en.json @@ -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", diff --git a/messages/es.json b/messages/es.json index 50f8d9dd4..0b5b87658 100644 --- a/messages/es.json +++ b/messages/es.json @@ -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", diff --git a/messages/fr.json b/messages/fr.json index 122199d74..c26d0256e 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -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", diff --git a/messages/it.json b/messages/it.json index 743cb5e34..7ab45c824 100644 --- a/messages/it.json +++ b/messages/it.json @@ -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", diff --git a/messages/pl.json b/messages/pl.json index 7d344e71d..39e58d11e 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -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", diff --git a/src/components/settings/__tests__/privacy-section.test.tsx b/src/components/settings/__tests__/privacy-section.test.tsx index 56aebc8cb..288a064d4 100644 --- a/src/components/settings/__tests__/privacy-section.test.tsx +++ b/src/components/settings/__tests__/privacy-section.test.tsx @@ -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( {node} @@ -56,5 +66,31 @@ describe(" — 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( + + + + + , + ); + // Skeleton rows render while the summary is loading; no retention day-count. + expect(html).toContain('data-slot="skeleton"'); + expect(html).not.toContain("365"); }); }); diff --git a/src/components/settings/privacy-section.tsx b/src/components/settings/privacy-section.tsx index 511877d30..6aa6534f4 100644 --- a/src/components/settings/privacy-section.tsx +++ b/src/components/settings/privacy-section.tsx @@ -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("/api/settings/privacy-summary"), enabled: isAuthenticated, @@ -99,7 +104,7 @@ export function PrivacySection() { description={t("settings.privacy.stored.description")} className="mb-4" /> -
    +
    • {t("settings.privacy.stored.measurements")}
    • {t("settings.privacy.stored.medications")}
    • {t("settings.privacy.stored.moodLabs")}
    • @@ -117,27 +122,35 @@ export function PrivacySection() { description={t("settings.privacy.retention.description")} className="mb-4" /> -
        - {summary && ( - <> -
      • - {t("settings.privacy.retention.coach", { - days: summary.retention.coachMessagesDays, - })} -
      • -
      • - {t("settings.privacy.retention.audit", { - days: summary.retention.auditLogDays, - })} -
      • -
      • - {t("settings.privacy.retention.delivery", { - days: summary.retention.deliveryLogDays, - })} -
      • - - )} -
      + {summaryLoading ? ( +
      + + + +
      + ) : summaryError || !summary ? ( +

      + {t("settings.privacy.retention.unavailable")} +

      + ) : ( +
        +
      • + {t("settings.privacy.retention.coach", { + days: summary.retention.coachMessagesDays, + })} +
      • +
      • + {t("settings.privacy.retention.audit", { + days: summary.retention.auditLogDays, + })} +
      • +
      • + {t("settings.privacy.retention.delivery", { + days: summary.retention.deliveryLogDays, + })} +
      • +
      + )} {/* Export */} diff --git a/src/components/settings/security-section/index.tsx b/src/components/settings/security-section/index.tsx index 53c8b34f9..f2e9a5ac5 100644 --- a/src/components/settings/security-section/index.tsx +++ b/src/components/settings/security-section/index.tsx @@ -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() { - - ); }