"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")}
-
{t("settings.ai.deviceCodeStep1")}{" "}
{deviceCode.verificationUrl}
-
{t("settings.ai.deviceCodeStep2")}
{deviceCode.userCode}
navigator.clipboard?.writeText(deviceCode.userCode)
}
>
{t("settings.ai.deviceCodeCopy")}
- {t("settings.ai.deviceCodeStep3")}
{devicePolling && (
{t("settings.ai.deviceCodeWaiting")}
)}
) : (
)}
);
}