mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
Research Mode was the opt-in behind the estimated GLP-1 drug-level curve: turn it on, read a disclaimer, get the chart. The chart stopped consulting the flag several releases ago and has painted for every account since, so the switch on Settings → Advanced changed no screen in either position. An inert control that promises to hide a chart is worse than no control. The curve is simply part of the medication page now. Gone with the switch: the acknowledgment dialog, `/api/auth/me/research-mode`, the shared status DTO and its gate helper, the disclaimer-version stamp, the query key, and the three user columns the acknowledgment wrote (migration 0321). The Privacy page's second button pointed at the retired control and goes too. Settings → Advanced is now what it reads as: reset the data, delete the account.
207 lines
7.0 KiB
TypeScript
207 lines
7.0 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* v1.23 — "Data & Privacy" dashboard (P7).
|
|
*
|
|
* One coherent pane that ASSEMBLES already-shipped machinery — it does not
|
|
* rebuild any of it. Each block either embeds an existing card (active
|
|
* sessions, security activity, trusted devices) or links to the existing
|
|
* surface that owns the action (export incl. the passphrase option, account
|
|
* deletion / data reset, AI privacy mode, research mode). The retention +
|
|
* encryption facts are read from the server so the copy stays truthful, and
|
|
* the backup↔deletion lag is disclosed honestly.
|
|
*/
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import Link from "next/link";
|
|
import {
|
|
Database,
|
|
Download,
|
|
ShieldCheck,
|
|
Trash2,
|
|
Clock,
|
|
Sparkles,
|
|
Info,
|
|
} from "lucide-react";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { SettingsCardActions } from "@/components/settings/_card-actions";
|
|
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";
|
|
import { SecuritySessionsCard } from "@/components/settings/security-sessions-card";
|
|
import { SecurityActivityCard } from "@/components/settings/security-activity-card";
|
|
import { TrustedDevicesCard } from "@/components/settings/trusted-devices-card";
|
|
import { useAuth } from "@/hooks/use-auth";
|
|
import { useTranslations } from "@/lib/i18n/context";
|
|
import { queryKeys } from "@/lib/query-keys";
|
|
import { apiGet } from "@/lib/api/api-fetch";
|
|
|
|
interface PrivacySummary {
|
|
retention: {
|
|
coachMessagesDays: number;
|
|
auditLogDays: number;
|
|
deliveryLogDays: number;
|
|
};
|
|
encryption: {
|
|
algorithm: string;
|
|
columnCount: number;
|
|
modelCount: number;
|
|
};
|
|
}
|
|
|
|
export function PrivacySection() {
|
|
const { t } = useTranslations();
|
|
const { isAuthenticated } = useAuth();
|
|
|
|
const {
|
|
data: summary,
|
|
isLoading: summaryLoading,
|
|
isError: summaryError,
|
|
} = useQuery({
|
|
queryKey: queryKeys.privacySummary(),
|
|
queryFn: () => apiGet<PrivacySummary>("/api/settings/privacy-summary"),
|
|
enabled: isAuthenticated,
|
|
});
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Encryption at rest */}
|
|
<SettingsCard>
|
|
<SettingsCardHeader
|
|
icon={ShieldCheck}
|
|
title={t("settings.privacy.encryption.title")}
|
|
/>
|
|
<div className="space-y-3">
|
|
<p className="text-sm">
|
|
{summary
|
|
? t("settings.privacy.encryption.summary", {
|
|
algorithm: summary.encryption.algorithm,
|
|
columns: summary.encryption.columnCount,
|
|
models: summary.encryption.modelCount,
|
|
})
|
|
: t("settings.privacy.encryption.statement")}
|
|
</p>
|
|
</div>
|
|
</SettingsCard>
|
|
|
|
{/* What's stored */}
|
|
<SettingsCard>
|
|
<SettingsCardHeader
|
|
icon={Database}
|
|
title={t("settings.privacy.stored.title")}
|
|
/>
|
|
<ul className="text-muted-foreground list-disc space-y-1 text-sm">
|
|
<li>{t("settings.privacy.stored.measurements")}</li>
|
|
<li>{t("settings.privacy.stored.medications")}</li>
|
|
<li>{t("settings.privacy.stored.moodLabs")}</li>
|
|
<li>{t("settings.privacy.stored.coach")}</li>
|
|
<li>{t("settings.privacy.stored.integrations")}</li>
|
|
<li>{t("settings.privacy.stored.security")}</li>
|
|
</ul>
|
|
</SettingsCard>
|
|
|
|
{/* Retention */}
|
|
<SettingsCard>
|
|
<SettingsCardHeader
|
|
icon={Clock}
|
|
title={t("settings.privacy.retention.title")}
|
|
description={t("settings.privacy.retention.description")}
|
|
/>
|
|
{summaryLoading ? (
|
|
<div className="space-y-2">
|
|
<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 text-sm">
|
|
{t("settings.privacy.retention.unavailable")}
|
|
</p>
|
|
) : (
|
|
<ul className="text-muted-foreground space-y-1 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 */}
|
|
<SettingsCard>
|
|
<SettingsCardHeader
|
|
icon={Download}
|
|
title={t("settings.privacy.export.title")}
|
|
description={t("settings.privacy.export.description")}
|
|
/>
|
|
<SettingsCardActions align="start">
|
|
<Button asChild variant="outline" size="sm">
|
|
<Link href="/settings/export">
|
|
{t("settings.privacy.export.openExport")}
|
|
</Link>
|
|
</Button>
|
|
<Button asChild variant="outline" size="sm">
|
|
<Link href="/settings/gesundheitsakte">
|
|
{t("settings.privacy.export.openHealthRecord")}
|
|
</Link>
|
|
</Button>
|
|
</SettingsCardActions>
|
|
</SettingsCard>
|
|
|
|
{/* Delete / reset — with the honest backup lag disclosure */}
|
|
<SettingsCard>
|
|
<SettingsCardHeader
|
|
icon={Trash2}
|
|
title={t("settings.privacy.delete.title")}
|
|
description={t("settings.privacy.delete.description")}
|
|
/>
|
|
<SettingsInfoTile icon={Info} tone="warning">
|
|
{t("settings.privacy.delete.backupLag")}
|
|
</SettingsInfoTile>
|
|
<SettingsCardActions align="start">
|
|
<Button asChild variant="outline" size="sm">
|
|
<Link href="/settings/advanced">
|
|
{t("settings.privacy.delete.openAdvanced")}
|
|
</Link>
|
|
</Button>
|
|
</SettingsCardActions>
|
|
</SettingsCard>
|
|
|
|
{/* Privacy posture. The AI privacy mode is what is left to decide here:
|
|
research mode is gone, so the second button pointed at a page that
|
|
no longer carries the control it named. */}
|
|
<SettingsCard>
|
|
<SettingsCardHeader
|
|
icon={Sparkles}
|
|
title={t("settings.privacy.posture.title")}
|
|
description={t("settings.privacy.posture.description")}
|
|
/>
|
|
<SettingsCardActions align="start">
|
|
<Button asChild variant="outline" size="sm">
|
|
<Link href="/settings/ai">
|
|
{t("settings.privacy.posture.openAi")}
|
|
</Link>
|
|
</Button>
|
|
</SettingsCardActions>
|
|
</SettingsCard>
|
|
|
|
{/* Active sessions + trusted devices + security activity (embedded) */}
|
|
<SecuritySessionsCard isAuthenticated={isAuthenticated} />
|
|
<TrustedDevicesCard isAuthenticated={isAuthenticated} />
|
|
<SecurityActivityCard isAuthenticated={isAuthenticated} />
|
|
</div>
|
|
);
|
|
}
|