mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
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.
183 lines
6.5 KiB
TypeScript
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>
|
|
);
|
|
}
|