mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
feat(settings): allow removing saved AI provider key
Closes the v1.4.6 deferred polish item. When the user has saved a key
for the active provider (OpenAI, Anthropic, or Local), a "Remove saved
key" button now sits next to Save / Test connection. Clicking opens an
AlertDialog confirmation, then PATCHes `{ <provider>Key: null }` — the
existing `/api/user/ai-provider` route already treats null/empty as
"clear" (encrypted column set to NULL).
Provider, model, and base URL stay untouched so users keep their
selection while just rotating the secret. The button hides itself when
no key is stored, so the only state where it shows is exactly the one
where it's actionable.
Insights for the wiped provider fall back to the admin/Codex provider
on the next request — surfaced in the confirm-dialog body so the
behaviour is explicit.
i18n: 4 new keys under `settings.ai.*` (`removeKeyCta`, `keyRemoved`,
`removeKeyConfirmTitle`, `removeKeyConfirmBody`), EN canonical, DE
mirror. Existing `i18n-locale-integrity.test.ts` enforces parity.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
38f12df737
commit
788c8ad335
3 files changed
+99
No files matched your search
@@ -1069,6 +1069,10 @@
|
||||
"savedShort": "(gespeichert)",
|
||||
"saveCta": "Speichern",
|
||||
"testCta": "Verbindung testen",
|
||||
"removeKeyCta": "Gespeicherten Key entfernen",
|
||||
"keyRemoved": "Gespeicherter Key entfernt",
|
||||
"removeKeyConfirmTitle": "Gespeicherten API-Key entfernen?",
|
||||
"removeKeyConfirmBody": "Insights für diesen Provider fallen auf den Admin-/Codex-Provider zurück, bis du einen neuen Key speicherst.",
|
||||
"saved": "Gespeichert",
|
||||
"saveFailed": "Speichern fehlgeschlagen",
|
||||
"testFailed": "Test fehlgeschlagen",
|
||||
|
||||
@@ -1069,6 +1069,10 @@
|
||||
"savedShort": "(saved)",
|
||||
"saveCta": "Save",
|
||||
"testCta": "Test connection",
|
||||
"removeKeyCta": "Remove saved key",
|
||||
"keyRemoved": "Saved key removed",
|
||||
"removeKeyConfirmTitle": "Remove saved API key?",
|
||||
"removeKeyConfirmBody": "Insights for this provider will fall back to the admin/Codex provider until you save a new key.",
|
||||
"saved": "Saved",
|
||||
"saveFailed": "Save failed",
|
||||
"testFailed": "Test failed",
|
||||
|
||||
@@ -4,6 +4,17 @@ import { useEffect, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Loader2, RefreshCw, Save, Sparkles, Trash2 } from "lucide-react";
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -639,6 +650,48 @@ function UserAIProviderSubsection() {
|
||||
},
|
||||
});
|
||||
|
||||
// v1.5 phase-5 — "Remove saved key" closes the v1.4.6 deferred polish.
|
||||
// Sends an explicit `null` for whichever provider key is stored so the
|
||||
// PATCH route clears it (the route already handles the empty-string /
|
||||
// null case). Provider/model/baseUrl are left alone so the user can
|
||||
// keep their selection while just rotating the secret.
|
||||
const removeKeyMutation = useMutation({
|
||||
mutationFn: async (target: "openaiKey" | "anthropicKey" | "localKey") => {
|
||||
const res = await fetch("/api/user/ai-provider", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ [target]: null }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!res.ok) throw new Error(json.error || t("settings.ai.saveFailed"));
|
||||
},
|
||||
onSuccess: () => {
|
||||
setSaveMsg(t("settings.ai.keyRemoved"));
|
||||
setSaveOk(true);
|
||||
queryClient.invalidateQueries({ queryKey: ["user", "ai-provider"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["insights"] });
|
||||
},
|
||||
onError: (e) => {
|
||||
setSaveMsg(
|
||||
e instanceof Error ? e.message : t("settings.ai.errorGeneric"),
|
||||
);
|
||||
setSaveOk(false);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Maps the currently selected provider to its stored-key flag and the
|
||||
* PATCH-body field name. Returns null when there is no saved key for
|
||||
* the active provider — the "Remove saved key" button hides itself.
|
||||
*/
|
||||
const removableKey: "openaiKey" | "anthropicKey" | "localKey" | null = (() => {
|
||||
if (provider === "OPENAI" && data?.hasOpenaiKey) return "openaiKey";
|
||||
if (provider === "ANTHROPIC" && data?.hasAnthropicKey)
|
||||
return "anthropicKey";
|
||||
if (provider === "LOCAL" && data?.hasLocalKey) return "localKey";
|
||||
return null;
|
||||
})();
|
||||
|
||||
async function runTest() {
|
||||
setTesting(true);
|
||||
setTestMsg(null);
|
||||
@@ -862,6 +915,44 @@ function UserAIProviderSubsection() {
|
||||
)}
|
||||
{t("settings.ai.testCta")}
|
||||
</Button>
|
||||
{removableKey && (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-destructive"
|
||||
disabled={removeKeyMutation.isPending}
|
||||
>
|
||||
{removeKeyMutation.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{t("settings.ai.removeKeyCta")}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{t("settings.ai.removeKeyConfirmTitle")}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("settings.ai.removeKeyConfirmBody")}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
onClick={() => removeKeyMutation.mutate(removableKey)}
|
||||
>
|
||||
{t("settings.ai.removeKeyCta")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{saveMsg && (
|
||||
|
||||
Reference in new issue
Block a user