'use client' import { useState, useEffect } from 'react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { useSettings, useUpdateSettings } from '@/hooks/use-settings' import { Settings, Container, Key, Server, Wrench, Eye, EyeOff, Save, RotateCcw, Loader2, AlertCircle, CheckCircle, RefreshCw, Shield, AlertTriangle, X, Mail, Bell, Send, Database, HardDrive, } from 'lucide-react' import { Switch } from '@/components/ui/switch' interface SettingFieldProps { settingKey: string label: string description?: string placeholder?: string type?: 'text' | 'password' | 'number' value: string encrypted?: boolean maskedValue?: string onChange: (key: string, value: string) => void } function SettingField({ settingKey, label, description, placeholder, type = 'text', value, encrypted, maskedValue, onChange, }: SettingFieldProps) { const [showPassword, setShowPassword] = useState(false) const [localValue, setLocalValue] = useState(value) const [isDirty, setIsDirty] = useState(false) useEffect(() => { if (!isDirty) { setLocalValue(value) } }, [value, isDirty]) const handleChange = (newValue: string) => { setLocalValue(newValue) setIsDirty(true) onChange(settingKey, newValue) } const isPassword = type === 'password' || encrypted return (
handleChange(e.target.value)} className={`h-10 bg-background/50 border-muted-foreground/20 focus:border-primary/50 focus:ring-2 focus:ring-primary/20 transition-all ${ isPassword ? 'pr-12' : '' } ${isDirty ? 'border-blue-300 dark:border-blue-700' : ''}`} /> {isPassword && ( )}
{description && (

{description}

)}
) } interface SettingsSectionProps { title: string description: string icon: React.ReactNode iconBgColor: string iconColor: string gradientFrom?: string children: React.ReactNode } function SettingsSection({ title, description, icon, iconBgColor, iconColor, gradientFrom = 'from-card', children }: SettingsSectionProps) { return (
{icon}

{title}

{description}

{children}
) } export default function SettingsPage() { const { data, isLoading, error, refetch, isFetching } = useSettings() const updateMutation = useUpdateSettings() const [formValues, setFormValues] = useState>({}) const [saveMessage, setSaveMessage] = useState<{ type: 'success' | 'error'; message: string } | null>(null) const [testEmail, setTestEmail] = useState('') const [testingEmail, setTestingEmail] = useState(false) const [testEmailResult, setTestEmailResult] = useState<{ success: boolean; message: string } | null>(null) const [testingStorage, setTestingStorage] = useState(false) const [testStorageResult, setTestStorageResult] = useState<{ success: boolean; message: string } | null>(null) // Initialize form values from fetched settings useEffect(() => { if (data?.settings) { const initial: Record = {} for (const setting of data.settings) { // For encrypted values, don't pre-fill (user must re-enter) initial[setting.key] = setting.encrypted ? '' : setting.value } setFormValues(initial) } }, [data]) const handleChange = (key: string, value: string) => { setFormValues((prev) => ({ ...prev, [key]: value })) } const handleSave = async () => { setSaveMessage(null) // Convert form values to settings array const settingsToUpdate = Object.entries(formValues) .filter(([, value]) => value !== '') // Only include non-empty values .map(([key, value]) => ({ key, value })) try { await updateMutation.mutateAsync({ settings: settingsToUpdate }) setSaveMessage({ type: 'success', message: 'Settings saved successfully!' }) // Reset form values after successful save setFormValues({}) setTimeout(() => setSaveMessage(null), 5000) } catch (err) { setSaveMessage({ type: 'error', message: err instanceof Error ? err.message : 'Failed to save settings', }) } } const handleReset = () => { setFormValues({}) if (data?.settings) { const initial: Record = {} for (const setting of data.settings) { initial[setting.key] = setting.encrypted ? '' : setting.value } setFormValues(initial) } setSaveMessage(null) } const handleTestEmail = async () => { setTestingEmail(true) setTestEmailResult(null) try { const response = await fetch('/api/v1/admin/settings/email/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ testEmail: testEmail || undefined }), }) const result = await response.json() if (result.success) { setTestEmailResult({ success: true, message: result.emailSent ? `Connection successful! Test email sent to ${testEmail}` : 'SMTP connection successful!', }) } else { setTestEmailResult({ success: false, message: result.error || 'Connection test failed', }) } } catch { setTestEmailResult({ success: false, message: 'Failed to test email connection', }) } finally { setTestingEmail(false) } } const handleTestStorage = async () => { setTestingStorage(true) setTestStorageResult(null) try { const response = await fetch('/api/v1/admin/settings/storage/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), }) const result = await response.json() setTestStorageResult({ success: result.success, message: result.message || (result.success ? 'Connection successful!' : 'Connection failed'), }) } catch { setTestStorageResult({ success: false, message: 'Failed to test storage connection', }) } finally { setTestingStorage(false) } } const getSettingValue = (key: string) => formValues[key] ?? '' const getSettingMasked = (key: string) => { const setting = data?.settings.find((s) => s.key === key) return setting?.maskedValue } const isEncrypted = (key: string) => { const setting = data?.settings.find((s) => s.key === key) return setting?.encrypted ?? false } if (isLoading) { return (

Loading settings...

) } if (error) { return (

Failed to load settings

There was an error loading the system settings. Please try again.

) } const hasChanges = Object.values(formValues).some((v) => v !== '') return (
{/* Hero Header */}
{/* Background decoration */}
{/* Title and description */}

System Settings

Configure system-wide settings for provisioning and integrations

{/* Action buttons */}
{/* Save Message Toast */} {saveMessage && (
{saveMessage.type === 'success' ? ( ) : ( )}
{saveMessage.message}
)} {/* Docker Runner Section */} } iconBgColor="bg-blue-100 dark:bg-blue-900/30" iconColor="text-blue-600 dark:text-blue-400" gradientFrom="from-card" >
{/* Docker Hub Section */} } iconBgColor="bg-sky-100 dark:bg-sky-900/30" iconColor="text-sky-600 dark:text-sky-400" gradientFrom="from-card" >
{/* Gitea Registry Section */} } iconBgColor="bg-amber-100 dark:bg-amber-900/30" iconColor="text-amber-600 dark:text-amber-400" gradientFrom="from-card" >
{/* Hub Configuration Section */} } iconBgColor="bg-purple-100 dark:bg-purple-900/30" iconColor="text-purple-600 dark:text-purple-400" gradientFrom="from-card" >
{/* Warning Box */}

Important Warning

Changing the encryption key will prevent decryption of previously encrypted values. Only change this if you understand the implications and have backed up your data.

{/* Provisioning Defaults Section */} } iconBgColor="bg-emerald-100 dark:bg-emerald-900/30" iconColor="text-emerald-600 dark:text-emerald-400" gradientFrom="from-card" >
{/* Email Configuration Section */} } iconBgColor="bg-pink-100 dark:bg-pink-900/30" iconColor="text-pink-600 dark:text-pink-400" gradientFrom="from-card" >
{/* TLS Toggle */}

Enable secure connection (recommended)

handleChange('email.smtp.secure', checked ? 'true' : 'false')} />
{/* Test Connection */}

Test Email Connection

setTestEmail(e.target.value)} className="flex-1" />

Save your changes first, then test. Leave email empty to just test connection, or enter an email to send a test message.

{testEmailResult && (
{testEmailResult.success ? ( ) : ( )} {testEmailResult.message}
)}
{/* Notifications Section */} } iconBgColor="bg-orange-100 dark:bg-orange-900/30" iconColor="text-orange-600 dark:text-orange-400" gradientFrom="from-card" > {/* Master Toggle */}

Master switch for all email notifications

handleChange('notifications.enabled', checked ? 'true' : 'false')} />
{/* Recipients */}
handleChange('notifications.recipients', e.target.value)} />

JSON array of email addresses to receive notifications

{/* Cooldown */}
{/* Container Alerts */}

Container Alerts

Alert when containers crash unexpectedly

handleChange('notifications.container.crashes.enabled', checked ? 'true' : 'false')} />

Alert when containers are killed due to memory limits

handleChange('notifications.container.oom.enabled', checked ? 'true' : 'false')} />

Alert when containers restart unexpectedly

handleChange('notifications.container.restarts.enabled', checked ? 'true' : 'false')} />
{/* Error Alerts */}

Error Alerts

Alert on critical-level errors detected in logs

handleChange('notifications.errors.critical.enabled', checked ? 'true' : 'false')} />

Alert on error-level messages (higher volume)

handleChange('notifications.errors.error.enabled', checked ? 'true' : 'false')} />
{/* Server Stats Alerts */}

Server Stats Alerts

{/* CPU */}

Alert when CPU exceeds threshold

handleChange('notifications.stats.cpu.enabled', checked ? 'true' : 'false')} />
handleChange('notifications.stats.cpu.threshold', e.target.value)} className="w-20 h-8 text-sm" /> %
{/* Memory */}

Alert when memory exceeds threshold

handleChange('notifications.stats.memory.enabled', checked ? 'true' : 'false')} />
handleChange('notifications.stats.memory.threshold', e.target.value)} className="w-20 h-8 text-sm" /> %
{/* Disk */}

Alert when disk usage exceeds threshold

handleChange('notifications.stats.disk.enabled', checked ? 'true' : 'false')} />
handleChange('notifications.stats.disk.threshold', e.target.value)} className="w-20 h-8 text-sm" /> %
{/* Object Storage Section */} } iconBgColor="bg-cyan-100 dark:bg-cyan-900/30" iconColor="text-cyan-600 dark:text-cyan-400" gradientFrom="from-card" >
{/* SSL Toggle */}

Enable secure connection to storage server

handleChange('storage.use_ssl', checked ? 'true' : 'false')} />
{/* Test Connection */}

Test Storage Connection

Save your changes first, then test. This will verify connection to the bucket.

{testStorageResult && (
{testStorageResult.success ? ( ) : ( )} {testStorageResult.message}
)}
) }