From 360a11523e1dd44bd05720184e628be7fb9b5098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Mon, 6 Apr 2026 11:56:35 +0200 Subject: [PATCH] feat(ui): replace API key input with ChatGPT OAuth connect button Settings now shows "Mit ChatGPT verbinden" button instead of API key input field. Shows connection status, connected-since date, and disconnect option. Handles OAuth callback query params with toast. Co-Authored-By: Claude Opus 4.6 (1M context) --- messages/de.json | 3 + messages/en.json | 3 + src/app/settings/page.tsx | 174 ++++++++++++++++++++++---------------- 3 files changed, 106 insertions(+), 74 deletions(-) diff --git a/messages/de.json b/messages/de.json index 8ecb0f66b..2d63584af 100644 --- a/messages/de.json +++ b/messages/de.json @@ -756,6 +756,9 @@ "apiKeyStored": "Key hinterlegt — neuen eingeben zum Ersetzen", "apiKeySaved": "API-Key gespeichert", "apiKeyRemoved": "API-Key entfernt", + "codexConnected": "ChatGPT erfolgreich verbunden! KI-Insights sind jetzt aktiv.", + "codexDisconnected": "ChatGPT-Verbindung getrennt.", + "codexConnectionFailed": "ChatGPT-Verbindung fehlgeschlagen. Bitte versuche es erneut.", "rawData": "Rohdaten mitsenden", "rawDataOnDescription": "Es werden aggregierte Kennzahlen plus anonymisierte Rohdaten der letzten 30 Tage gesendet. Dazu zählen einzelne Messpunkte je Bereich (z. B. Gewicht, Blutdruck, Puls) mit Zeitbezug, damit die KI zeitliche Muster, Ausreißer und Zusammenhänge zuverlässiger erkennen kann. Nicht übertragen werden Name, E-Mail oder direkte Kontodaten.", "rawDataOffDescription": "Es werden nur zusammengefasste Werte gesendet (z. B. Durchschnitt, Trend, Minimum/Maximum). Keine einzelnen Messpunkte und keine exakten Zeitbezüge. Das ist datensparsamer, die KI kann dafür aber weniger fein auf Muster und kurzfristige Schwankungen eingehen.", diff --git a/messages/en.json b/messages/en.json index 21c7d60b0..14e782747 100644 --- a/messages/en.json +++ b/messages/en.json @@ -756,6 +756,9 @@ "apiKeyStored": "Key stored — enter new one to replace", "apiKeySaved": "API key saved", "apiKeyRemoved": "API key removed", + "codexConnected": "ChatGPT connected successfully! AI insights are now active.", + "codexDisconnected": "ChatGPT connection removed.", + "codexConnectionFailed": "ChatGPT connection failed. Please try again.", "rawData": "Send raw data", "rawDataOnDescription": "Aggregated metrics plus anonymized raw points from the last 30 days are sent. This includes per-metric measurements (for example weight, blood pressure, pulse) with time context so AI can detect patterns, outliers, and correlations more reliably. Name, email, and direct account identifiers are not transmitted.", "rawDataOffDescription": "Only summarized metrics are sent (for example averages, trends, minimum/maximum). No individual points and no exact time context. This is more privacy-preserving, but AI can be less precise for short-term patterns and fluctuations.", diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 2a528cd55..b41e7c10d 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -2627,8 +2627,7 @@ function InsightsSettingsSection({ }) { const { t } = useTranslations(); const queryClient = useQueryClient(); - const [apiKey, setApiKey] = useState(""); - const [saving, setSaving] = useState(false); + const [disconnecting, setDisconnecting] = useState(false); const [regenerating, setRegenerating] = useState(false); const [msg, setMsg] = useState(null); const [msgType, setMsgType] = useState<"success" | "error" | null>(null); @@ -2641,6 +2640,7 @@ function InsightsSettingsSection({ const json = await res.json(); return json.data as { codexStatus: string; + codexConnectedAt: string | null; hasAdminKey: boolean; privacyMode: string; lastInsightAt: string | null; @@ -2664,23 +2664,44 @@ function InsightsSettingsSection({ }, }); - async function handleSaveKey(e: React.FormEvent) { - e.preventDefault(); - setSaving(true); - setMsg(null); - setMsgType(null); - try { - await updateSettings.mutateAsync({ apiKey: apiKey.trim() }); - setMsg( - apiKey.trim() ? t("settings.apiKeySaved") : t("settings.apiKeyRemoved"), - ); + // Handle OAuth callback query params + useEffect(() => { + const params = new URLSearchParams(window.location.search); + if (params.get("codex_connected") === "true") { + setMsg(t("settings.codexConnected")); setMsgType("success"); - setApiKey(""); + queryClient.invalidateQueries({ queryKey: ["insights"] }); + window.history.replaceState({}, "", window.location.pathname); + } else if (params.get("codex_error")) { + setMsg(t("settings.codexConnectionFailed")); + setMsgType("error"); + window.history.replaceState({}, "", window.location.pathname); + } + }, [queryClient, t]); + + const hasProvider = settings?.codexStatus === "connected" || settings?.hasAdminKey; + + async function handleConnect() { + window.location.href = "/api/auth/codex/authorize"; + } + + async function handleDisconnect() { + setDisconnecting(true); + setMsg(null); + try { + const res = await fetch("/api/auth/codex/disconnect", { method: "DELETE" }); + if (!res.ok) { + const json = await res.json(); + throw new Error(json.error); + } + setMsg(t("settings.codexDisconnected")); + setMsgType("success"); + queryClient.invalidateQueries({ queryKey: ["insights"] }); } catch (err) { setMsg(err instanceof Error ? err.message : t("settings.savingError")); setMsgType("error"); } finally { - setSaving(false); + setDisconnecting(false); } } @@ -2731,9 +2752,19 @@ function InsightsSettingsSection({

{t("settings.kiInsights")}

- {(settings?.codexStatus === "connected" || settings?.hasAdminKey) && ( + {settings?.codexStatus === "connected" && ( - {t("settings.configured")} + ChatGPT verbunden + + )} + {settings?.codexStatus !== "connected" && settings?.hasAdminKey && ( + + Admin-KI aktiv + + )} + {settings?.codexStatus === "expired" && ( + + Verbindung abgelaufen )} {settings?.lastInsightAt && ( @@ -2747,62 +2778,57 @@ function InsightsSettingsSection({

{t("settings.kiInsightsDescription")}

-
-

- {t("settings.kiInsightsBenefitsTitle")} -

-
    -
  • {t("settings.kiInsightsBenefit1")}
  • -
  • {t("settings.kiInsightsBenefit2")}
  • -
  • {t("settings.kiInsightsBenefit3")}
  • -
-

- {t("settings.createApiKeyHint")}{" "} - - {t("settings.createApiKeyLinkLabel")} - -

-
- {/* API Key */} -
- setApiKey(e.target.value)} - placeholder={ - (settings?.codexStatus === "connected" || settings?.hasAdminKey) ? t("settings.apiKeyStored") : "sk-..." - } - className="flex-1" - /> - - {(settings?.codexStatus === "connected" || settings?.hasAdminKey) && ( - + {/* ChatGPT Connection */} +
+ {settings?.codexStatus === "connected" ? ( +
+
+

ChatGPT verbunden

+

+ Insights werden über dein ChatGPT-Abo generiert — keine zusätzlichen Kosten. + {settings.codexConnectedAt && ( + <> Verbunden seit {formatDateTime(settings.codexConnectedAt)}. + )} +

+
+ +
+ ) : ( +
+
+

Mit ChatGPT verbinden

+

+ Verbinde dein ChatGPT Pro/Max-Konto um KI-gestützte Gesundheitsanalysen basierend auf + aktuellen medizinischen Leitlinien zu erhalten. Keine zusätzlichen API-Kosten. +

+
+ + {settings?.hasAdminKey && ( +

+ Alternativ nutzt HealthLog den vom Administrator konfigurierten KI-Anbieter. +

+ )} +
)} - +
{msg && (

{t("settings.rawData")}

- {settings.privacyMode === "raw" + {settings?.privacyMode === "raw" ? t("settings.rawDataOnDescription") : t("settings.rawDataOffDescription")}

- {settings.privacyMode === "raw" && ( + {settings?.privacyMode === "raw" && (
{t("settings.rawDataWarning")}
@@ -2841,7 +2867,7 @@ function InsightsSettingsSection({ )} {/* Regenerate Reports */} - {(settings?.codexStatus === "connected" || settings?.hasAdminKey) && ( + {hasProvider && (