Files
HealthLog/src/components/admin/danger-zone-section.tsx
T
Marc-André Bombeck a4b204ffe4 refactor(ui): retire the raw dracula utilities for semantic tokens
Migrate every remaining *-dracula-* utility site (127 lint warnings,
193 class occurrences) onto the semantic vocabulary. The swaps are
pixel-identical in both themes — each target token aliases the same
primitive the utility read, including the light-theme AA overrides:

- green/orange/cyan/red → success/warning/info/destructive
- purple → primary (same value in both themes)
- pink → the new --brand-pink token (Coach gradient, Apple-Health
  badge, recording mic): dark #ff79c6 — 6.86:1 card / 5.97:1 bg;
  light #a3134d — 7.32:1 card / 6.83:1 bg (computed)
- yellow sites resolve to their status meaning: warn-level log icon,
  update-available pill and offline-geo hint → warning; the admin
  helpful-rate ramp → success/warning/destructive; the confidence
  meter mirrors the coverage-meter precedent (medium + low share the
  caution tint, the lit count carries the distinction)
- the confidence-badge low tier dropped dead *-dracula-comment/*
  classes (no generated utility) for border-border + bg-muted/50

Close two rule blind spots the sweep surfaced — directional borders
(border-l-dracula-red) and the parenthesised CSS-var shorthand
(bg-(--dracula-green)) — and flip healthlog/no-dracula-utility from
warn to error so the class cannot regrow. var(--dracula-*) references
in chart code stay: they are token references the light theme
overrides, and the chart palette is pinned visually.
2026-07-05 21:28:09 +02:00

183 lines
6.5 KiB
TypeScript

"use client";
import { useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { AlertTriangle, Loader2, Trash2 } from "lucide-react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
import { apiDelete } from "@/lib/api/api-fetch";
/**
* The literal an admin must type to arm the wipe. Mirrors the Backups
* `RESTORE` gate — and matches the server's typed-token defence at
* `src/app/api/admin/data/route.ts` (which 422s any other `confirm`).
*/
const WIPE_CONFIRM_TOKEN = "DELETE ALL";
export function DangerZoneSection() {
const { t } = useTranslations();
const queryClient = useQueryClient();
const [wipeMsg, setWipeMsg] = useState<string | null>(null);
// Typed-confirmation gate. The destructive button stays disabled until
// the admin types `DELETE ALL` verbatim, matching the Backups Restore
// dialog (which forces typing `RESTORE`) so the FAR more destructive
// global wipe is not the weaker gate. Reset whenever the dialog closes.
const [open, setOpen] = useState(false);
const [typed, setTyped] = useState("");
const matched = typed.trim() === WIPE_CONFIRM_TOKEN;
const wipeAllData = useMutation({
mutationFn: async () => {
// Idempotency-Key prevents a double-submit from re-running the
// destructive transaction, matching the Backups restore path.
const idempotencyKey = `admin-data-wipe-${crypto.randomUUID()}`;
return apiDelete<{
measurements: number;
intakeEvents: number;
medications: number;
apiTokens: number;
withingsConnections: number;
authChallenges: number;
notificationChannels: number;
pushSubscriptions: number;
telegramScheduledDeletions: number;
}>(
"/api/admin/data",
{ confirm: WIPE_CONFIRM_TOKEN },
{ headers: { "Idempotency-Key": idempotencyKey } },
);
},
onSuccess: (data) => {
queryClient.invalidateQueries();
setWipeMsg(
t("admin.deletedResult", {
measurements: data.measurements,
medications: data.medications,
intakeEvents: data.intakeEvents,
apiTokens: data.apiTokens,
withingsConnections: data.withingsConnections,
authChallenges: data.authChallenges,
notificationChannels: data.notificationChannels,
pushSubscriptions: data.pushSubscriptions,
telegramScheduledDeletions: data.telegramScheduledDeletions,
}),
);
},
onError: (err: Error) => {
setWipeMsg(err.message);
},
});
return (
// v1.18.10 (W7) — the card SURFACE is neutral like every other admin /
// settings section (`SettingsCard`: bg-card + border-border): a red card
// edge reads as alarming, not premium. The danger signal lives ONLY in
// the destructive delete button + its typed-confirmation dialog, which
// are alarm enough. The header keeps the `AlertTriangle` icon so the
// section still flags its intent.
<SettingsCard>
<SettingsCardHeader
icon={AlertTriangle}
title={t("admin.deleteAllData")}
description={t("admin.deleteAllDescription")}
status={
<AlertDialog
open={open}
onOpenChange={(next) => {
setOpen(next);
if (!next) setTyped("");
}}
>
<AlertDialogTrigger asChild>
<Button
variant="destructive"
size="sm"
disabled={wipeAllData.isPending}
className="min-h-11"
>
{wipeAllData.isPending ? (
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<Trash2 className="h-3.5 w-3.5" />
)}
{t("admin.deleteButton")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{t("admin.deleteAllConfirm")}
</AlertDialogTitle>
<AlertDialogDescription>
{t("admin.deleteAllConfirmDescription")}
</AlertDialogDescription>
</AlertDialogHeader>
<div className="space-y-2">
<Label htmlFor="wipe-confirm-prompt">
{t("admin.deleteAllPromptLabel", {
token: WIPE_CONFIRM_TOKEN,
})}
</Label>
<Input
id="wipe-confirm-prompt"
value={typed}
onChange={(e) => setTyped(e.target.value)}
autoComplete="off"
spellCheck={false}
placeholder={WIPE_CONFIRM_TOKEN}
/>
</div>
<AlertDialogFooter>
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
<AlertDialogAction
variant="destructive"
disabled={!matched || wipeAllData.isPending}
aria-busy={wipeAllData.isPending || undefined}
onClick={(e) => {
if (!matched) {
e.preventDefault();
return;
}
setOpen(false);
setTyped("");
wipeAllData.mutate();
}}
>
{wipeAllData.isPending && (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
)}
{t("admin.finalDelete")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
}
/>
{wipeMsg && (
<p
className={`mt-3 pl-7 text-sm ${
wipeAllData.isError ? "text-destructive" : "text-success"
}`}
>
{wipeMsg}
</p>
)}
</SettingsCard>
);
}