"use client"; import { useState } from "react"; import { useFormState } from "react-dom"; import { Alert, Box, Button, Card, CardContent, Checkbox, FormControlLabel, MenuItem, Stack, TextField, Typography } from "@mui/material"; import type { GeneralSettings, AuthentikSettings, MetricsSettings, LoggingSettings, DnsSettings } from "@/src/lib/settings"; import { updateCloudflareSettingsAction, updateGeneralSettingsAction, updateAuthentikSettingsAction, updateMetricsSettingsAction, updateLoggingSettingsAction, updateDnsSettingsAction, updateInstanceModeAction, updateSlaveMasterTokenAction, createSlaveInstanceAction, deleteSlaveInstanceAction, toggleSlaveInstanceAction, syncSlaveInstancesAction } from "./actions"; type Props = { general: GeneralSettings | null; cloudflare: { hasToken: boolean; zoneId?: string; accountId?: string; }; authentik: AuthentikSettings | null; metrics: MetricsSettings | null; logging: LoggingSettings | null; dns: DnsSettings | null; instanceSync: { mode: "standalone" | "master" | "slave"; modeFromEnv: boolean; tokenFromEnv: boolean; overrides: { general: boolean; cloudflare: boolean; authentik: boolean; metrics: boolean; logging: boolean; dns: 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; }; }; export default function SettingsClient({ general, cloudflare, authentik, metrics, logging, dns, 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 [instanceModeState, instanceModeFormAction] = useFormState(updateInstanceModeAction, null); const [slaveTokenState, slaveTokenFormAction] = useFormState(updateSlaveMasterTokenAction, null); const [slaveInstanceState, slaveInstanceFormAction] = useFormState(createSlaveInstanceAction, null); const [syncState, syncFormAction] = useFormState(syncSlaveInstancesAction, 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); return ( Settings Configure organization-wide defaults and DNS automation. Instance Sync Choose whether this instance acts independently, pushes configuration to slave nodes, or pulls configuration from a master. {instanceSync.modeFromEnv && ( Instance mode is configured via INSTANCE_MODE environment variable and cannot be changed at runtime. )} {instanceModeState?.message && ( {instanceModeState.message} )} Standalone Master Slave {isSlave && ( Master Connection {instanceSync.tokenFromEnv && ( Sync token is configured via INSTANCE_SYNC_TOKEN environment variable and cannot be changed at runtime. )} {slaveTokenState?.message && ( {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. )} } label="Remove existing token" disabled={!instanceSync.slave?.hasToken || instanceSync.tokenFromEnv} /> {instanceSync.slave?.lastSyncAt ? `Last sync: ${instanceSync.slave.lastSyncAt}${instanceSync.slave.lastSyncError ? ` (${instanceSync.slave.lastSyncError})` : ""}` : "No sync payload has been received yet."} )} {isMaster && ( Slave Instances {slaveInstanceState?.message && ( {slaveInstanceState.message} )} {syncState?.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 instances (via INSTANCE_SLAVES) {instanceSync.master.envInstances.map((instance, index) => ( {instance.name} {instance.url} Configured via environment variable ))} )} {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 {generalState?.message && ( {generalState.message} )} {isSlave && ( setGeneralOverride(event.target.checked)} /> } label="Override master settings" /> )} Cloudflare DNS Configure a Cloudflare API token with Zone.DNS Edit permissions to enable DNS-01 challenges for wildcard certificates. {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 && ( {cloudflareState.message} )} {isSlave && ( setCloudflareOverride(event.target.checked)} /> } label="Override master settings" /> )} } label="Remove existing token" disabled={!cloudflare.hasToken || (isSlave && !cloudflareOverride)} /> DNS Resolvers Configure custom DNS resolvers for ACME DNS-01 challenges. These resolvers will be used to verify DNS records during certificate issuance. {dnsState?.message && ( {dnsState.message} )} {isSlave && ( setDnsOverride(event.target.checked)} /> } label="Override master settings" /> )} } label="Enable custom DNS resolvers" /> Custom DNS resolvers are useful when your DNS provider has slow propagation or when using split-horizon DNS. Common public resolvers: 1.1.1.1 (Cloudflare), 8.8.8.8 (Google), 9.9.9.9 (Quad9). Authentik Defaults Set default Authentik forward authentication values. These will be pre-filled when creating new proxy hosts but can be customized per host. {authentikState?.message && ( {authentikState.message} )} {isSlave && ( setAuthentikOverride(event.target.checked)} /> } label="Override master settings" /> )} Metrics & Monitoring Enable Caddy metrics exposure for monitoring with Prometheus, Grafana, or other observability tools. Metrics will be available at http://caddy:{metrics?.port ?? 9090}/metrics on a separate port (NOT the admin API port for security). {metricsState?.message && ( {metricsState.message} )} {isSlave && ( setMetricsOverride(event.target.checked)} /> } label="Override master settings" /> )} } label="Enable metrics endpoint" /> After enabling metrics, configure your monitoring tool to scrape http://caddy-proxy-manager-caddy:{metrics?.port ?? 9090}/metrics from within the Docker network. To expose metrics externally, add a port mapping like "{metrics?.port ?? 9090}:{metrics?.port ?? 9090}" in docker-compose.yml. Access Logging Enable HTTP access logging to track all requests going through your proxy hosts. Logs will be stored in the caddy-logs directory and mounted at /logs/access.log inside the container. {loggingState?.message && ( {loggingState.message} )} {isSlave && ( setLoggingOverride(event.target.checked)} /> } label="Override master settings" /> )} } label="Enable access logging" /> JSON Console (Common Log Format) Access logs are stored in the caddy-logs Docker volume. You can view them with: docker exec caddy-proxy-manager-caddy tail -f /logs/access.log ); }