"use client"; import { useState } from "react"; import { useFormState } from "react-dom"; import { Cloud, Globe, Network, Pin, Activity, ScrollText, Settings2, UserCheck, MapPin, } from "lucide-react"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { StatusChip } from "@/components/ui/StatusChip"; import type { GeneralSettings, AuthentikSettings, MetricsSettings, LoggingSettings, DnsSettings, UpstreamDnsResolutionSettings, GeoBlockSettings, } from "@/lib/settings"; import { GeoBlockFields } from "@/components/proxy-hosts/GeoBlockFields"; import { updateCloudflareSettingsAction, updateGeneralSettingsAction, updateAuthentikSettingsAction, updateMetricsSettingsAction, updateLoggingSettingsAction, updateDnsSettingsAction, updateUpstreamDnsResolutionSettingsAction, updateInstanceModeAction, updateSlaveMasterTokenAction, createSlaveInstanceAction, deleteSlaveInstanceAction, toggleSlaveInstanceAction, syncSlaveInstancesAction, updateGeoBlockSettingsAction, } from "./actions"; import { ReactNode } from "react"; // ─── Alert helpers ──────────────────────────────────────────────────────────── function StatusAlert({ message, success }: { message: string; success: boolean }) { return ( {message} ); } function InfoAlert({ children }: { children: ReactNode }) { return ( {children} ); } function WarnAlert({ children }: { children: ReactNode }) { return ( {children} ); } // ─── Section card ───────────────────────────────────────────────────────────── type AccentConfig = { border: string; icon: string }; function SettingSection({ icon, title, description, accent, children, }: { icon: ReactNode; title: string; description?: string; accent: AccentConfig; children: ReactNode; }) { return (
{icon}

{title}

{description && (

{description}

)}
{children}
); } // ─── Accents ────────────────────────────────────────────────────────────────── const A: Record = { sync: { border: "border-l-violet-500", icon: "border-violet-500/30 bg-violet-500/10 text-violet-500" }, general: { border: "border-l-zinc-400", icon: "border-zinc-500/30 bg-zinc-500/10 text-zinc-500" }, cloudflare: { border: "border-l-orange-500", icon: "border-orange-500/30 bg-orange-500/10 text-orange-500" }, dns: { border: "border-l-cyan-500", icon: "border-cyan-500/30 bg-cyan-500/10 text-cyan-500" }, upstreamDns:{ border: "border-l-emerald-500", icon: "border-emerald-500/30 bg-emerald-500/10 text-emerald-500" }, authentik: { border: "border-l-purple-500", icon: "border-purple-500/30 bg-purple-500/10 text-purple-500" }, metrics: { border: "border-l-rose-500", icon: "border-rose-500/30 bg-rose-500/10 text-rose-500" }, logging: { border: "border-l-amber-500", icon: "border-amber-500/30 bg-amber-500/10 text-amber-500" }, geoblock: { border: "border-l-teal-500", icon: "border-teal-500/30 bg-teal-500/10 text-teal-500" }, }; // ─── Props ──────────────────────────────────────────────────────────────────── type Props = { general: GeneralSettings | null; cloudflare: { hasToken: boolean; zoneId?: string; accountId?: string; }; authentik: AuthentikSettings | null; metrics: MetricsSettings | null; logging: LoggingSettings | null; dns: DnsSettings | null; upstreamDnsResolution: UpstreamDnsResolutionSettings | null; globalGeoBlock?: GeoBlockSettings | null; instanceSync: { mode: "standalone" | "master" | "slave"; modeFromEnv: boolean; tokenFromEnv: boolean; overrides: { general: boolean; cloudflare: boolean; authentik: boolean; metrics: boolean; logging: boolean; dns: boolean; upstreamDnsResolution: boolean; }; slave: { hasToken: boolean; lastSyncAt: string | null; lastSyncError: string | null; } | null; master: { instances: Array<{ id: number; name: string; base_url: string; enabled: boolean; last_sync_at: string | null; last_sync_error: string | null; }>; envInstances: Array<{ name: string; url: string; }>; } | null; }; }; // ─── Component ──────────────────────────────────────────────────────────────── export default function SettingsClient({ general, cloudflare, authentik, metrics, logging, dns, upstreamDnsResolution, globalGeoBlock, instanceSync }: Props) { const [generalState, generalFormAction] = useFormState(updateGeneralSettingsAction, null); const [cloudflareState, cloudflareFormAction] = useFormState(updateCloudflareSettingsAction, null); const [authentikState, authentikFormAction] = useFormState(updateAuthentikSettingsAction, null); const [metricsState, metricsFormAction] = useFormState(updateMetricsSettingsAction, null); const [loggingState, loggingFormAction] = useFormState(updateLoggingSettingsAction, null); const [dnsState, dnsFormAction] = useFormState(updateDnsSettingsAction, null); const [upstreamDnsResolutionState, upstreamDnsResolutionFormAction] = useFormState( updateUpstreamDnsResolutionSettingsAction, null ); const [instanceModeState, instanceModeFormAction] = useFormState(updateInstanceModeAction, null); const [slaveTokenState, slaveTokenFormAction] = useFormState(updateSlaveMasterTokenAction, null); const [slaveInstanceState, slaveInstanceFormAction] = useFormState(createSlaveInstanceAction, null); const [syncState, syncFormAction] = useFormState(syncSlaveInstancesAction, null); const [geoBlockState, geoBlockFormAction] = useFormState(updateGeoBlockSettingsAction, null); const isSlave = instanceSync.mode === "slave"; const isMaster = instanceSync.mode === "master"; const [generalOverride, setGeneralOverride] = useState(instanceSync.overrides.general); const [cloudflareOverride, setCloudflareOverride] = useState(instanceSync.overrides.cloudflare); const [authentikOverride, setAuthentikOverride] = useState(instanceSync.overrides.authentik); const [metricsOverride, setMetricsOverride] = useState(instanceSync.overrides.metrics); const [loggingOverride, setLoggingOverride] = useState(instanceSync.overrides.logging); const [dnsOverride, setDnsOverride] = useState(instanceSync.overrides.dns); const [upstreamDnsResolutionOverride, setUpstreamDnsResolutionOverride] = useState( instanceSync.overrides.upstreamDnsResolution ); return (

Settings

Configure organization-wide defaults and DNS automation.

{/* ── Instance Sync ── */} } title="Instance Sync" description="Choose whether this instance acts independently, pushes configuration to slave nodes, or pulls configuration from a master." accent={A.sync} >
{instanceSync.modeFromEnv && ( Instance mode is configured via INSTANCE_MODE environment variable and cannot be changed at runtime. )} {instanceModeState?.message && ( )}
{isSlave && (

Master Connection

{instanceSync.tokenFromEnv && ( Sync token is configured via INSTANCE_SYNC_TOKEN environment variable and cannot be changed at runtime. )} {slaveTokenState?.message && ( )} {instanceSync.slave?.hasToken && !instanceSync.tokenFromEnv && ( A master sync token is configured. Leave the token field blank to keep it, or select “Remove existing token” to delete it. )}
{instanceSync.slave?.lastSyncError ? ( {instanceSync.slave?.lastSyncAt ? `Last sync: ${instanceSync.slave.lastSyncAt} (${instanceSync.slave.lastSyncError})` : "No sync payload has been received yet."} ) : ( {instanceSync.slave?.lastSyncAt ? `Last sync: ${instanceSync.slave.lastSyncAt}` : "No sync payload has been received yet."} )}
)} {isMaster && (

Slave Instances

{slaveInstanceState?.message && ( )}
{syncState?.message && ( )}
{instanceSync.master?.instances.length === 0 && instanceSync.master?.envInstances.length === 0 && ( No slave instances configured yet. )} {instanceSync.master?.envInstances && instanceSync.master.envInstances.length > 0 && ( <>

Environment-configured (INSTANCE_SLAVES)

{instanceSync.master.envInstances.map((instance, index) => (

{instance.name}

{instance.url}

))} )} {instanceSync.master?.instances && instanceSync.master.instances.length > 0 && (

UI-configured instances

)} {instanceSync.master?.instances.map((instance) => (

{instance.name}

{instance.base_url}

{instance.last_sync_at ? `Last sync: ${instance.last_sync_at}` : "No sync yet"} {instance.last_sync_error && ( {instance.last_sync_error} )}
))}
)}
{/* ── General ── */} } title="General" accent={A.general} >
{generalState?.message && ( )} {isSlave && (
setGeneralOverride(!!v)} />
)}
{/* ── Cloudflare DNS ── */} } title="Cloudflare DNS" description="Configure a Cloudflare API token with Zone.DNS Edit permissions to enable DNS-01 challenges for wildcard certificates." accent={A.cloudflare} > {cloudflare.hasToken && ( A Cloudflare API token is already configured. Leave the token field blank to keep it, or select “Remove existing token” to delete it. )}
{cloudflareState?.message && ( )} {isSlave && (
setCloudflareOverride(!!v)} />
)}
{/* ── DNS Resolvers ── */} } title="DNS Resolvers" description="Configure custom DNS resolvers for ACME DNS-01 challenges. These resolvers will be used to verify DNS records during certificate issuance." accent={A.dns} >
{dnsState?.message && ( )} {isSlave && (
setDnsOverride(!!v)} />
)}