"use client" import { AlertTriangle, Check, Loader2, LockKeyhole, ShieldCheck, } from "lucide-react" import { useCallback, useEffect, useState } from "react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Switch } from "@/components/ui/switch" import { useDictionary } from "@/hooks/use-dictionary" import { SETTING_GROUPS, SETTINGS_BY_GROUP, } from "@/lib/admin/settings-registry" import { getApiEndpoint } from "@/lib/base-path" import { formatMessage } from "@/lib/i18n/utils" import { cn } from "@/lib/utils" import { type AdminProvider, adminFetch, type EnvProvider, isSecretValue, SESSION_PASSWORD_KEY, type SettingState, type SettingsMap, savedTextOf, } from "./admin-shared" import { ModelsSection } from "./models-section" import { SettingField } from "./setting-field" // ── Page ───────────────────────────────────────────────────────────── const NAV_GROUP_IDS = ["models", ...SETTING_GROUPS.map((g) => g.id)] export default function AdminPage() { const dict = useDictionary() // Localized group title/description, keyed by group id const groupText = (id: string) => ( dict.admin.groups as Record< string, { title: string; description: string } | undefined > )[id] const navItems = NAV_GROUP_IDS.map((id) => ({ id, title: id === "models" ? dict.admin.models : (groupText(id)?.title ?? id), })) const [password, setPassword] = useState("") const [authedPassword, setAuthedPassword] = useState(null) const [authError, setAuthError] = useState("") const [authLoading, setAuthLoading] = useState(false) const [writable, setWritable] = useState(true) // Models section state const [providers, setProviders] = useState([]) const [envProviders, setEnvProviders] = useState([]) const [savedProviders, setSavedProviders] = useState("[]") const providersDirty = JSON.stringify(providers) !== savedProviders // General settings state const [settings, setSettings] = useState({}) const [pending, setPending] = useState>({}) const [errors, setErrors] = useState>({}) const [enabledGroups, setEnabledGroups] = useState>( {}, ) const [saving, setSaving] = useState(false) const [saveMessage, setSaveMessage] = useState<{ ok: boolean text: string } | null>(null) const [activeGroup, setActiveGroup] = useState("models") const dirtyCount = Object.keys(pending).length + (providersDirty ? 1 : 0) const applySettingsResponse = useCallback( (data: { writable: boolean; settings: SettingState[] }) => { setWritable(data.writable) const map: SettingsMap = {} for (const s of data.settings) map[s.key] = s setSettings(map) // Seed each toggle once from whether the group has configured // values; don't stomp a user's explicit toggle on later saves setEnabledGroups((prev) => { const next = { ...prev } for (const group of SETTING_GROUPS) { if (!group.toggleable || group.id in next) continue next[group.id] = !!SETTINGS_BY_GROUP.get(group.id)?.some( (d) => map[d.key]?.source !== "default", ) } return next }) }, [], ) const applyProvidersResponse = useCallback( (data: { providers: AdminProvider[] envProviders?: EnvProvider[] }) => { setProviders(data.providers) setSavedProviders(JSON.stringify(data.providers)) setEnvProviders(data.envProviders ?? []) }, [], ) const login = useCallback( async (pw: string) => { setAuthLoading(true) setAuthError("") try { const [settingsData, providersData] = await Promise.all([ adminFetch("/api/admin/settings", pw), adminFetch("/api/admin/providers", pw), ]) applySettingsResponse(settingsData) applyProvidersResponse(providersData) setAuthedPassword(pw) sessionStorage.setItem(SESSION_PASSWORD_KEY, pw) } catch (err) { setAuthError( err instanceof Error ? err.message : dict.admin.loginFailed, ) } finally { setAuthLoading(false) } }, [applySettingsResponse, applyProvidersResponse, dict], ) // Restore session on mount useEffect(() => { const stored = sessionStorage.getItem(SESSION_PASSWORD_KEY) if (stored) void login(stored) }, [login]) // Warn before leaving with unsaved changes const hasDirty = dirtyCount > 0 useEffect(() => { if (!hasDirty) return const handler = (e: BeforeUnloadEvent) => { e.preventDefault() // Some browsers only show the prompt when returnValue is set e.returnValue = "" } window.addEventListener("beforeunload", handler) return () => window.removeEventListener("beforeunload", handler) }, [hasDirty]) // Highlight the section currently in view in the sidebar useEffect(() => { if (!authedPassword) return const observer = new IntersectionObserver( (entries) => { const visible = entries .filter((e) => e.isIntersecting) .sort( (a, b) => a.boundingClientRect.top - b.boundingClientRect.top, ) if (visible[0]) setActiveGroup(visible[0].target.id) }, { rootMargin: "-10% 0px -50% 0px" }, ) for (const id of NAV_GROUP_IDS) { const el = document.getElementById(id) if (el) observer.observe(el) } return () => observer.disconnect() }, [authedPassword]) const handleChange = useCallback( (key: string, value: string | null) => { setSaveMessage(null) setErrors((prev) => { if (!(key in prev)) return prev const next = { ...prev } delete next[key] return next }) setPending((prev) => { const state = settings[key] const isRevert = value !== null && state?.source === "file" && !isSecretValue(state?.value) && value === savedTextOf(state) const isNoop = value === "" && (!state || state.source !== "file") && !isSecretValue(state?.value) if (isRevert || isNoop) { const next = { ...prev } delete next[key] return next } return { ...prev, [key]: value === "" ? null : value } }) }, [settings], ) // Toggling a group off stages deletion of its saved values so the // feature actually turns off on save; toggling on drops those deletions. const handleGroupToggle = useCallback( (groupId: string, enabled: boolean) => { setSaveMessage(null) setEnabledGroups((prev) => ({ ...prev, [groupId]: enabled })) const keys = (SETTINGS_BY_GROUP.get(groupId) ?? []).map( (d) => d.key, ) setPending((prev) => { const next = { ...prev } for (const key of keys) { if (!enabled) { // Stage deletion only for values currently set if (settings[key]?.source !== "default") next[key] = null } else if (next[key] === null) { delete next[key] } } return next }) }, [settings], ) const handleSave = useCallback(async () => { if (!authedPassword || dirtyCount === 0) return setSaving(true) setSaveMessage(null) setErrors({}) try { if (providersDirty) { const data = await adminFetch( "/api/admin/providers", authedPassword, { method: "PUT", body: JSON.stringify({ providers }) }, ) applyProvidersResponse(data) } if (Object.keys(pending).length > 0) { const res = await fetch(getApiEndpoint("/api/admin/settings"), { method: "PUT", headers: { "Content-Type": "application/json", "x-admin-password": authedPassword, }, body: JSON.stringify({ values: pending }), }) const data = await res.json().catch(() => ({})) if (!res.ok) { // Per-field validation errors come back as {errors: {...}} if (data.errors) { setErrors(data.errors) const firstKey = Object.keys(data.errors)[0] document.getElementById(`setting-${firstKey}`)?.focus() throw new Error(dict.admin.invalidSettings) } throw new Error( data.error || `Request failed (${res.status})`, ) } applySettingsResponse(data) setPending({}) } setSaveMessage({ ok: true, text: dict.admin.saved, }) setTimeout(() => setSaveMessage(null), 4000) } catch (err) { setSaveMessage({ ok: false, text: err instanceof Error ? err.message : dict.admin.saveFailed, }) } finally { setSaving(false) } }, [ authedPassword, pending, providers, providersDirty, dirtyCount, applySettingsResponse, applyProvidersResponse, dict, ]) // ── Login screen ───────────────────────────────────────────────── if (!authedPassword) { return (
{ e.preventDefault() void login(password) }} >

{dict.admin.loginPrompt}

setPassword(e.target.value)} />

{authError}

) } // ── Settings screen ────────────────────────────────────────────── return (

{dict.admin.precedence}

{!writable && (
)}
{/* Models section */}

{dict.admin.models}

{dict.admin.modelsDescription}

{ setSaveMessage(null) setProviders(next) }} />
{/* Registry-driven groups */} {SETTING_GROUPS.map((group) => { const defs = SETTINGS_BY_GROUP.get(group.id) ?? [] const groupOff = group.toggleable && !enabledGroups[group.id] const fieldsDisabled = !writable || saving || !!groupOff const gt = groupText(group.id) const title = gt?.title ?? group.title return (

{title}

{group.toggleable && ( )}

{gt?.description ?? group.description}

{defs.map((def) => ( handleChange(def.key, v) } /> ))}
) })}
{/* Always-mounted live region so save results are announced */}

{saveMessage?.text ?? ""}

{(dirtyCount > 0 || saveMessage) && (

{saveMessage?.ok && (

{dirtyCount > 0 && (
)}
)}
) }