'use client'; import { useState, useEffect, startTransition } from 'react'; import { api } from '~/trpc/react'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { Badge } from './ui/badge'; import { ContextualHelpIcon } from './ContextualHelpIcon'; import { LoadingModal } from './LoadingModal'; import { ConfirmationModal } from './ConfirmationModal'; import { RefreshCw, AlertTriangle, CheckCircle } from 'lucide-react'; import { useRegisterModal } from './modal/ModalStackProvider'; interface InstalledScript { id: number; script_name: string; container_id: string | null; server_id: number | null; server_name: string | null; server_ip: string | null; server_user: string | null; server_password: string | null; server_auth_type: string | null; server_ssh_key: string | null; server_ssh_key_passphrase: string | null; server_ssh_port: number | null; server_color: string | null; installation_date: string; status: 'in_progress' | 'success' | 'failed'; output_log: string | null; execution_mode: 'local' | 'ssh'; container_status?: 'running' | 'stopped' | 'unknown'; web_ui_ip: string | null; web_ui_port: number | null; } interface LXCSettingsModalProps { isOpen: boolean; script: InstalledScript | null; onClose: () => void; onSave: () => void; } export function LXCSettingsModal({ isOpen, script, onClose, onSave: _onSave }: LXCSettingsModalProps) { useRegisterModal(isOpen, { id: 'lxc-settings-modal', allowEscape: true, onClose }); const [activeTab, setActiveTab] = useState('common'); const [showConfirmation, setShowConfirmation] = useState(false); const [showResultModal, setShowResultModal] = useState(false); const [resultType, setResultType] = useState<'success' | 'error' | null>(null); const [resultMessage, setResultMessage] = useState(null); const [error, setError] = useState(null); const [hasChanges, setHasChanges] = useState(false); const [forceSync] = useState(false); const [isSaving, setIsSaving] = useState(false); const [formData, setFormData] = useState({ arch: '', cores: 0, memory: 0, hostname: '', swap: 0, onboot: false, ostype: '', unprivileged: false, net_name: '', net_bridge: '', net_hwaddr: '', net_ip_type: 'dhcp', net_ip: '', net_gateway: '', net_type: '', net_vlan: 0, rootfs_storage: '', rootfs_size: '', feature_keyctl: false, feature_nesting: false, feature_fuse: false, feature_mount: '', tags: '', advanced_config: '' }); // tRPC hooks const { data: configData, isLoading, refetch } = api.installedScripts.getLXCConfig.useQuery( { scriptId: script?.id ?? 0, forceSync }, { enabled: !!script && isOpen } ); const saveMutation = api.installedScripts.saveLXCConfig.useMutation({ onSuccess: (data) => { console.log('Save mutation success data:', data); setIsSaving(false); setShowConfirmation(false); if (data.success) { setResultType('success'); setResultMessage(data.message ?? 'LXC configuration saved successfully'); setHasChanges(false); } else { console.log('Backend returned error:', data.error); setResultType('error'); setResultMessage(data.error ?? 'Failed to save configuration'); } setShowResultModal(true); }, onError: (err) => { console.log('Save mutation error:', err); setIsSaving(false); setShowConfirmation(false); setResultType('error'); setResultMessage(`Failed to save configuration: ${err.message}`); setShowResultModal(true); } }); const syncMutation = api.installedScripts.syncLXCConfig.useMutation({ onSuccess: (result) => { populateFormData(result); setHasChanges(false); }, onError: (err) => { setError(`Failed to sync configuration: ${err.message}`); } }); // Populate form data helper const populateFormData = (result: any) => { if (!result?.success) return; const config = result.config; setFormData({ arch: config.arch ?? '', cores: config.cores ?? 0, memory: config.memory ?? 0, hostname: config.hostname ?? '', swap: config.swap ?? 0, onboot: config.onboot === 1, ostype: config.ostype ?? '', unprivileged: config.unprivileged === 1, net_name: config.net_name ?? '', net_bridge: config.net_bridge ?? '', net_hwaddr: config.net_hwaddr ?? '', net_ip_type: config.net_ip_type ?? 'dhcp', net_ip: config.net_ip ?? '', net_gateway: config.net_gateway ?? '', net_type: config.net_type ?? '', net_vlan: config.net_vlan ?? 0, rootfs_storage: config.rootfs_storage ?? '', rootfs_size: config.rootfs_size ?? '', feature_keyctl: config.feature_keyctl === 1, feature_nesting: config.feature_nesting === 1, feature_fuse: config.feature_fuse === 1, feature_mount: config.feature_mount ?? '', tags: config.tags ?? '', advanced_config: config.advanced_config ?? '' }); }; // Load config when data arrives useEffect(() => { if (configData?.success) { populateFormData(configData); startTransition(() => { setHasChanges(false); }); } else if (configData && !configData.success) { startTransition(() => { setError(String(configData.error ?? 'Failed to load configuration')); }); } }, [configData]); const handleInputChange = (field: string, value: any): void => { // eslint-disable-next-line @typescript-eslint/no-unsafe-return setFormData((prev: any) => ({ ...prev, [field]: value })); setHasChanges(true); }; const handleSyncFromServer = () => { if (!script) return; setError(null); syncMutation.mutate({ scriptId: script.id }); }; const validateForm = () => { // Check required fields if (!formData.arch?.trim()) { setError('Architecture is required'); return false; } if (!formData.cores || formData.cores < 1) { setError('Cores must be at least 1'); return false; } if (!formData.memory || formData.memory < 128) { setError('Memory must be at least 128 MB'); return false; } if (!formData.hostname?.trim()) { setError('Hostname is required'); return false; } if (!formData.ostype?.trim()) { setError('OS Type is required'); return false; } if (!formData.rootfs_storage?.trim()) { setError('Root filesystem storage is required'); return false; } // Check if trying to decrease disk size const currentSize = configData?.config?.rootfs_size ?? '0G'; const newSize = formData.rootfs_size ?? '0G'; const currentSizeGB = parseFloat(String(currentSize)); const newSizeGB = parseFloat(String(newSize)); if (newSizeGB < currentSizeGB) { setError('Disk size cannot be decreased. Only increases are allowed for safety.'); return false; } return true; }; const handleSave = () => { setError(null); // Validate form - only show confirmation modal if no errors if (validateForm()) { setShowConfirmation(true); } }; const handleConfirmSave = () => { if (!script) return; setError(null); setIsSaving(true); setShowConfirmation(false); saveMutation.mutate({ scriptId: script.id, config: { ...formData, onboot: formData.onboot ? 1 : 0, unprivileged: formData.unprivileged ? 1 : 0, feature_keyctl: formData.feature_keyctl ? 1 : 0, feature_nesting: formData.feature_nesting ? 1 : 0, feature_fuse: formData.feature_fuse ? 1 : 0 } }); }; const handleResultModalClose = () => { setShowResultModal(false); setResultType(null); setResultMessage(null); // Refresh the data to show updated values void refetch(); }; if (!isOpen || !script) return null; return ( <>
{/* Header */}

LXC Settings

{script.container_id}
{/* Warning Banner */} {configData?.has_changes && (

Configuration Mismatch Detected

The cached configuration differs from the server. Click "Sync from Server" to get the latest version.

)} {/* Error Message */} {error && (

Error

{error}

)} {/* Content */}
{/* Tab Navigation */}
{/* Common Settings Tab */} {activeTab === 'common' && (
{/* Basic Configuration */}

Basic Configuration

handleInputChange('arch', e.target.value)} placeholder="amd64" />
handleInputChange('cores', parseInt(e.target.value) || 0)} min="1" />
handleInputChange('memory', parseInt(e.target.value) || 0)} min="128" />
handleInputChange('swap', parseInt(e.target.value) || 0)} min="0" />
handleInputChange('hostname', e.target.value)} placeholder="container-hostname" />
handleInputChange('ostype', e.target.value)} placeholder="debian" />
handleInputChange('onboot', e.target.checked)} className="h-4 w-4 text-primary focus:ring-primary border-border rounded" />
handleInputChange('unprivileged', e.target.checked)} className="h-4 w-4 text-primary focus:ring-primary border-border rounded" />
{/* Network Configuration */}

Network Configuration

handleInputChange('net_name', e.target.value)} placeholder="eth0" />
handleInputChange('net_bridge', e.target.value)} placeholder="vmbr0" />
handleInputChange('net_hwaddr', e.target.value)} placeholder="BC:24:11:2D:2D:AB" />
handleInputChange('net_type', e.target.value)} placeholder="veth" />
{formData.net_ip_type === 'static' && ( <>
handleInputChange('net_ip', e.target.value)} placeholder="10.10.10.164/24" />
handleInputChange('net_gateway', e.target.value)} placeholder="10.10.10.254" />
)}
handleInputChange('net_vlan', parseInt(e.target.value) || 0)} placeholder="Optional" />
{/* Storage */}

Storage

handleInputChange('rootfs_storage', e.target.value)} placeholder="PROX2-STORAGE2:vm-109-disk-0" />
handleInputChange('rootfs_size', e.target.value)} placeholder="4G" />

Disk size can only be increased for safety. Format: 4G, 8G, 16G, etc.

{/* Features */}

Features

handleInputChange('feature_keyctl', e.target.checked)} className="h-4 w-4 text-primary focus:ring-primary border-border rounded" />
handleInputChange('feature_nesting', e.target.checked)} className="h-4 w-4 text-primary focus:ring-primary border-border rounded" />
handleInputChange('feature_fuse', e.target.checked)} className="h-4 w-4 text-primary focus:ring-primary border-border rounded" />
handleInputChange('feature_mount', e.target.value)} placeholder="Additional features (comma-separated)" />
{/* Tags */}

Tags

handleInputChange('tags', e.target.value)} placeholder="community-script;pve-scripts-local" />
)} {/* Advanced Settings Tab */} {activeTab === 'advanced' && (