"use client"; /** * Operator panel for the shared central Codex (ChatGPT subscription) account. * * The operator connects ONE signed-in ChatGPT account here (device-code flow, * cookie-only `requireAdmin` on every backing route) and any user can opt into * it from their own AI settings. This is the ONE admin surface where the vendor * is named, consistent with the per-user Codex connect form — the operator is * knowingly linking a named account. * * The copy is loud and honest: it is a shared, signed-in account (not an API * key), subscription providers may use content to improve their models, the * account's 5-hour / weekly rate limits are shared across every opted-in user, * and it is never a default. OFF until the operator connects it. */ import { useEffect, useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Loader2, ShieldAlert, Sparkles, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { showErrorToast } from "@/lib/errors/error-toast"; import { Badge } from "@/components/ui/badge"; import { SettingsInfoTile } from "@/components/settings/_info-tile"; import { SettingsCardActions } from "@/components/settings/_card-actions"; import { Button } from "@/components/ui/button"; import { ConfirmButton } from "@/components/ui/confirm-button"; import { SettingsCard } from "@/components/settings/settings-card"; import { SettingsCardHeader } from "@/components/settings/_card-header"; import { apiFetchRaw, apiGet } from "@/lib/api/api-fetch"; import { formatDateTime } from "@/lib/format"; import { useTranslations } from "@/lib/i18n/context"; import { aiInputDependentKeys, invalidateKeys, queryKeys, } from "@/lib/query-keys"; import { InlineAction } from "@/components/ui/inline-action"; interface CentralCodexStatus { status: string; connectedAt: string | null; } export function CentralCodexSection() { const { t } = useTranslations(); const queryClient = useQueryClient(); const { data } = useQuery({ queryKey: queryKeys.adminCentralCodex(), queryFn: () => apiGet("/api/admin/central-codex"), }); const [deviceCode, setDeviceCode] = useState<{ userCode: string; verificationUrl: string; intervalSeconds: number; } | null>(null); const [devicePolling, setDevicePolling] = useState(false); const [disconnecting, setDisconnecting] = useState(false); const status = data?.status ?? "disconnected"; const isConnected = status === "connected"; async function handleConnect() { setDevicePolling(true); try { const res = await apiFetchRaw("/api/admin/central-codex/device-start", { method: "POST", }); const json = await res.json(); if (!res.ok) throw new Error(json.error || t("admin.centralCodex.error")); setDeviceCode({ userCode: json.data.userCode, verificationUrl: json.data.verificationUrl, intervalSeconds: json.data.intervalSeconds, }); } catch (err) { showErrorToast( err instanceof Error ? err.message : t("admin.centralCodex.error"), { cause: err }, ); setDevicePolling(false); } } useEffect(() => { if (!deviceCode) return; let cancelled = false; const intervalMs = Math.max(deviceCode.intervalSeconds, 3) * 1000; async function tick() { try { const res = await apiFetchRaw("/api/admin/central-codex/device-poll", { method: "POST", }); const json = await res.json(); if (cancelled) return; if (!res.ok) { throw new Error(json.error || t("admin.centralCodex.error")); } if (json.data?.status === "connected") { setDeviceCode(null); setDevicePolling(false); toast.success(t("admin.centralCodex.connected")); queryClient.invalidateQueries({ queryKey: queryKeys.adminCentralCodex(), }); void invalidateKeys(queryClient, aiInputDependentKeys); return; } if (!cancelled) setTimeout(tick, intervalMs); } catch (err) { if (cancelled) return; showErrorToast( err instanceof Error ? err.message : t("admin.centralCodex.error"), { cause: err }, ); setDeviceCode(null); setDevicePolling(false); } } const handle = setTimeout(tick, intervalMs); return () => { cancelled = true; clearTimeout(handle); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [deviceCode]); async function handleDisconnect() { setDisconnecting(true); try { const res = await apiFetchRaw("/api/admin/central-codex", { method: "DELETE", }); if (!res.ok) { const json = await res.json(); throw new Error(json.error || t("admin.centralCodex.error")); } toast.success(t("admin.centralCodex.disconnected")); queryClient.invalidateQueries({ queryKey: queryKeys.adminCentralCodex(), }); void invalidateKeys(queryClient, aiInputDependentKeys); } catch (err) { showErrorToast( err instanceof Error ? err.message : t("admin.centralCodex.error"), { cause: err }, ); } finally { setDisconnecting(false); } } return ( {t("admin.centralCodex.description")}

} status={ {isConnected ? t("admin.centralCodex.statusConnected") : status === "expired" ? t("admin.centralCodex.statusExpired") : t("admin.centralCodex.statusDisconnected")} } />

{t("admin.centralCodex.honesty")}

{data?.connectedAt && isConnected && (

{t("admin.centralCodex.connectedSince", { when: formatDateTime(data.connectedAt), })}

)} {isConnected ? ( } label={t("admin.centralCodex.disconnectButton")} title={t("admin.centralCodex.disconnectTitle")} body={t("admin.centralCodex.disconnectBody")} confirmLabel={t("admin.centralCodex.disconnectConfirm")} pending={disconnecting} onConfirm={handleDisconnect} /> ) : deviceCode ? (

{t("settings.ai.deviceCodeHeading")}

  1. {t("settings.ai.deviceCodeStep1")}{" "} {deviceCode.verificationUrl}
  2. {t("settings.ai.deviceCodeStep2")}
    {deviceCode.userCode} navigator.clipboard?.writeText(deviceCode.userCode) } > {t("settings.ai.deviceCodeCopy")}
  3. {t("settings.ai.deviceCodeStep3")}
{devicePolling && ( {t("settings.ai.deviceCodeWaiting")} )}
) : ( )}
); }