import { useState, useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '../components/ui/Card' import { Button } from '../components/ui/Button' import { Input } from '../components/ui/Input' import { Switch } from '../components/ui/Switch' import { Label } from '../components/ui/Label' import { Alert, AlertDescription } from '../components/ui/Alert' import { Badge } from '../components/ui/Badge' import { Skeleton } from '../components/ui/Skeleton' import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '../components/ui/Select' import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '../components/ui/Tooltip' import { toast } from '../utils/toast' import { getSettings, updateSetting, testPublicURL } from '../api/settings' import { getFeatureFlags, updateFeatureFlags } from '../api/featureFlags' import client from '../api/client' import { Server, RefreshCw, Save, Activity, Info, ExternalLink, CheckCircle2, XCircle, AlertTriangle } from 'lucide-react' import { ConfigReloadOverlay } from '../components/LoadingStates' import { WebSocketStatusCard } from '../components/WebSocketStatusCard' import { LanguageSelector } from '../components/LanguageSelector' import { cn } from '../utils/cn' interface HealthResponse { status: string service: string version: string git_commit: string build_time: string } interface UpdateInfo { current_version: string latest_version: string update_available: boolean release_url?: string } export default function SystemSettings() { const { t } = useTranslation() const queryClient = useQueryClient() const [caddyAdminAPI, setCaddyAdminAPI] = useState('http://localhost:2019') const [sslProvider, setSslProvider] = useState('auto') const [domainLinkBehavior, setDomainLinkBehavior] = useState('new_tab') const [publicURL, setPublicURL] = useState('') const [publicURLValid, setPublicURLValid] = useState(null) const [publicURLSaving, setPublicURLSaving] = useState(false) // Fetch Settings const { data: settings } = useQuery({ queryKey: ['settings'], queryFn: getSettings, }) // Update local state when settings load useEffect(() => { if (settings) { if (settings['caddy.admin_api']) setCaddyAdminAPI(settings['caddy.admin_api']) // Default to 'auto' if empty or invalid value if (settings['caddy.ssl_provider']) { const validProviders = ['auto', 'letsencrypt-staging', 'letsencrypt-prod', 'zerossl'] const provider = settings['caddy.ssl_provider'] setSslProvider(validProviders.includes(provider) ? provider : 'auto') } if (settings['ui.domain_link_behavior']) setDomainLinkBehavior(settings['ui.domain_link_behavior']) if (settings['app.public_url']) setPublicURL(settings['app.public_url']) } }, [settings]) // Validate Public URL with debouncing const validatePublicURL = async (url: string) => { if (!url) { setPublicURLValid(null) return } try { const response = await client.post('/settings/validate-url', { url }) setPublicURLValid(response.data.valid) } catch { setPublicURLValid(false) } } // Debounce validation useEffect(() => { const timer = setTimeout(() => { if (publicURL) { validatePublicURL(publicURL) } }, 300) return () => clearTimeout(timer) }, [publicURL]) // Fetch Health/System Status const { data: health, isLoading: isLoadingHealth } = useQuery({ queryKey: ['health'], queryFn: async (): Promise => { const response = await client.get('/health') return response.data }, }) // Test Public URL - Server-side connectivity test with SSRF protection const testPublicURLHandler = async () => { if (!publicURL) { toast.error(t('systemSettings.applicationUrl.invalidUrl')) return } setPublicURLSaving(true) try { const result = await testPublicURL(publicURL) if (result.reachable) { toast.success( result.message || `URL reachable (${result.latency?.toFixed(0)}ms)` ) } else { toast.error(result.error || 'URL not reachable') } } catch (error) { toast.error(error instanceof Error ? error.message : 'Test failed') } finally { setPublicURLSaving(false) } } // Check for Updates const { data: updateInfo, refetch: checkUpdates, isFetching: isCheckingUpdates, } = useQuery({ queryKey: ['updates'], queryFn: async (): Promise => { const response = await client.get('/system/updates') return response.data }, enabled: false, // Manual trigger }) const saveSettingsMutation = useMutation({ mutationFn: async () => { await updateSetting('caddy.admin_api', caddyAdminAPI, 'caddy', 'string') await updateSetting('caddy.ssl_provider', sslProvider, 'caddy', 'string') await updateSetting('ui.domain_link_behavior', domainLinkBehavior, 'ui', 'string') await updateSetting('app.public_url', publicURL, 'general', 'string') }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['settings'] }) toast.success(t('systemSettings.settingsSaved')) }, onError: (error: Error) => { toast.error(t('systemSettings.settingsFailed', { error: error.message })) }, }) // Feature Flags const { data: featureFlags, refetch: refetchFlags } = useQuery({ queryKey: ['feature-flags'], queryFn: getFeatureFlags, }) const featureToggles = useMemo( () => [ { key: 'feature.cerberus.enabled', label: t('systemSettings.features.cerberus'), tooltip: t('systemSettings.features.cerberusTooltip'), }, { key: 'feature.crowdsec.console_enrollment', label: t('systemSettings.features.crowdsecConsole'), tooltip: t('systemSettings.features.crowdsecConsoleTooltip'), }, { key: 'feature.uptime.enabled', label: t('systemSettings.features.uptimeMonitoring'), tooltip: t('systemSettings.features.uptimeMonitoringTooltip'), }, ], [t] ) const updateFlagMutation = useMutation({ mutationFn: async (payload: Record) => updateFeatureFlags(payload), onSuccess: () => { refetchFlags() toast.success(t('systemSettings.featureFlagUpdated')) }, onError: (err: unknown) => { const msg = err instanceof Error ? err.message : String(err) toast.error(t('systemSettings.featureFlagFailed', { error: msg })) }, }) // CrowdSec control // Determine loading message const { message, submessage } = updateFlagMutation.isPending ? { message: t('systemSettings.updatingFeatures'), submessage: t('systemSettings.applyingChanges') } : { message: t('common.loading'), submessage: t('systemSettings.pleaseWait') } // Loading skeleton for settings const SettingsSkeleton = () => (
{[1, 2, 3].map((i) => (
))}
) // Show skeleton while loading initial data if (!settings && !featureFlags) { return } return ( {updateFlagMutation.isPending && ( )}

{t('systemSettings.title')}

{/* Features */} {t('systemSettings.features.title')} {t('systemSettings.features.description')}
{featureFlags ? ( featureToggles.map(({ key, label, tooltip }) => (

{tooltip}

updateFlagMutation.mutate({ [key]: e.target.checked })} />
)) ) : (
)}
{/* General Configuration */} {t('systemSettings.general.title')} {t('systemSettings.general.description')}
setCaddyAdminAPI(e.target.value)} placeholder="http://localhost:2019" helperText={t('systemSettings.general.caddyAdminApiHelper')} />

{t('systemSettings.general.sslProviderHelper')}

{t('systemSettings.general.domainLinkBehaviorHelper')}

{t('systemSettings.general.languageHelper')}

{/* Application URL */} {t('systemSettings.applicationUrl.title')} {t('systemSettings.applicationUrl.description')} {t('systemSettings.applicationUrl.infoMessage')}
{ setPublicURL(e.target.value) }} placeholder="https://charon.example.com" className={cn( publicURLValid === false && 'border-red-500', publicURLValid === true && 'border-green-500' )} /> {publicURLValid !== null && ( publicURLValid ? ( ) : ( ) )}

{t('systemSettings.applicationUrl.helper')}

{publicURLValid === false && (

{t('systemSettings.applicationUrl.invalidUrl')}

)}
{!publicURL && ( {t('systemSettings.applicationUrl.notConfiguredWarning')} )}
{/* System Status */}
{t('systemSettings.systemStatus.title')}
{isLoadingHealth ? (
{[1, 2, 3, 4].map((i) => (
))}
) : health ? (

{health.service}

{health.status === 'healthy' ? t('dashboard.healthy') : t('dashboard.unhealthy')}

{health.version}

{health.build_time || t('systemSettings.systemStatus.notAvailable')}

{health.git_commit || t('systemSettings.systemStatus.notAvailable')}

) : ( {t('systemSettings.systemStatus.fetchError')} )}
{/* Update Check */} {t('systemSettings.updates.title')} {t('systemSettings.updates.description')} {updateInfo && ( <>

{updateInfo.current_version}

{updateInfo.latest_version}

{updateInfo.update_available ? ( {t('systemSettings.updates.newVersionAvailable')}{' '} {updateInfo.release_url && ( {t('systemSettings.updates.viewReleaseNotes')} )} ) : ( {t('systemSettings.updates.runningLatest')} )} )}
{/* WebSocket Connection Status */}
) }