'use client'; import { useState, useEffect, useRef } from 'react'; import { api } from '~/trpc/react'; import { Terminal } from './Terminal'; import { StatusBadge } from './Badge'; import { Button } from './ui/button'; import { ScriptInstallationCard } from './ScriptInstallationCard'; import { getContrastColor } from '../../lib/colorUtils'; interface InstalledScript { id: number; script_name: string; script_path: 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_color: string | null; installation_date: string; status: 'in_progress' | 'success' | 'failed'; output_log: string | null; } export function InstalledScriptsTab() { const [searchTerm, setSearchTerm] = useState(''); const [statusFilter, setStatusFilter] = useState<'all' | 'success' | 'failed' | 'in_progress'>('all'); const [serverFilter, setServerFilter] = useState('all'); const [sortField, setSortField] = useState<'script_name' | 'container_id' | 'server_name' | 'status' | 'installation_date'>('script_name'); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); const [updatingScript, setUpdatingScript] = useState<{ id: number; containerId: string; server?: any } | null>(null); const [editingScriptId, setEditingScriptId] = useState(null); const [editFormData, setEditFormData] = useState<{ script_name: string; container_id: string }>({ script_name: '', container_id: '' }); const [showAddForm, setShowAddForm] = useState(false); const [addFormData, setAddFormData] = useState<{ script_name: string; container_id: string; server_id: string }>({ script_name: '', container_id: '', server_id: 'local' }); const [showAutoDetectForm, setShowAutoDetectForm] = useState(false); const [autoDetectServerId, setAutoDetectServerId] = useState(''); const [autoDetectStatus, setAutoDetectStatus] = useState<{ type: 'success' | 'error' | null; message: string }>({ type: null, message: '' }); const [cleanupStatus, setCleanupStatus] = useState<{ type: 'success' | 'error' | null; message: string }>({ type: null, message: '' }); const cleanupRunRef = useRef(false); // Fetch installed scripts const { data: scriptsData, refetch: refetchScripts, isLoading } = api.installedScripts.getAllInstalledScripts.useQuery(); const { data: statsData } = api.installedScripts.getInstallationStats.useQuery(); const { data: serversData } = api.servers.getAllServers.useQuery(); // Delete script mutation const deleteScriptMutation = api.installedScripts.deleteInstalledScript.useMutation({ onSuccess: () => { void refetchScripts(); } }); // Update script mutation const updateScriptMutation = api.installedScripts.updateInstalledScript.useMutation({ onSuccess: () => { void refetchScripts(); setEditingScriptId(null); setEditFormData({ script_name: '', container_id: '' }); }, onError: (error) => { alert(`Error updating script: ${error.message}`); } }); // Create script mutation const createScriptMutation = api.installedScripts.createInstalledScript.useMutation({ onSuccess: () => { void refetchScripts(); setShowAddForm(false); setAddFormData({ script_name: '', container_id: '', server_id: 'local' }); }, onError: (error) => { alert(`Error creating script: ${error.message}`); } }); // Auto-detect LXC containers mutation const autoDetectMutation = api.installedScripts.autoDetectLXCContainers.useMutation({ onSuccess: (data) => { console.log('Auto-detect success:', data); void refetchScripts(); setShowAutoDetectForm(false); setAutoDetectServerId(''); // Show detailed message about what was added/skipped let statusMessage = data.message ?? 'Auto-detection completed successfully!'; if (data.skippedContainers && data.skippedContainers.length > 0) { const skippedNames = data.skippedContainers.map((c: any) => String(c.hostname)).join(', '); statusMessage += ` Skipped duplicates: ${skippedNames}`; } setAutoDetectStatus({ type: 'success', message: statusMessage }); // Clear status after 8 seconds (longer for detailed info) setTimeout(() => setAutoDetectStatus({ type: null, message: '' }), 8000); }, onError: (error) => { console.error('Auto-detect mutation error:', error); console.error('Error details:', { message: error.message, data: error.data }); setAutoDetectStatus({ type: 'error', message: error.message ?? 'Auto-detection failed. Please try again.' }); // Clear status after 5 seconds setTimeout(() => setAutoDetectStatus({ type: null, message: '' }), 5000); } }); // Cleanup orphaned scripts mutation const cleanupMutation = api.installedScripts.cleanupOrphanedScripts.useMutation({ onSuccess: (data) => { console.log('Cleanup success:', data); void refetchScripts(); if (data.deletedCount > 0) { setCleanupStatus({ type: 'success', message: `Cleanup completed! Removed ${data.deletedCount} orphaned script(s): ${data.deletedScripts.join(', ')}` }); } else { setCleanupStatus({ type: 'success', message: 'Cleanup completed! No orphaned scripts found.' }); } // Clear status after 8 seconds (longer for cleanup info) setTimeout(() => setCleanupStatus({ type: null, message: '' }), 8000); }, onError: (error) => { console.error('Cleanup mutation error:', error); setCleanupStatus({ type: 'error', message: error.message ?? 'Cleanup failed. Please try again.' }); // Clear status after 5 seconds setTimeout(() => setCleanupStatus({ type: null, message: '' }), 5000); } }); const scripts: InstalledScript[] = (scriptsData?.scripts as InstalledScript[]) ?? []; const stats = statsData?.stats; // Run cleanup when component mounts and scripts are loaded (only once) useEffect(() => { if (scripts.length > 0 && serversData?.servers && !cleanupMutation.isPending && !cleanupRunRef.current) { console.log('Running automatic cleanup check...'); cleanupRunRef.current = true; void cleanupMutation.mutate(); } }, [scripts.length, serversData?.servers, cleanupMutation]); // Filter and sort scripts const filteredScripts = scripts .filter((script: InstalledScript) => { const matchesSearch = script.script_name.toLowerCase().includes(searchTerm.toLowerCase()) || (script.container_id?.includes(searchTerm) ?? false) || (script.server_name?.toLowerCase().includes(searchTerm.toLowerCase()) ?? false); const matchesStatus = statusFilter === 'all' || script.status === statusFilter; const matchesServer = serverFilter === 'all' || (serverFilter === 'local' && !script.server_name) || (script.server_name === serverFilter); return matchesSearch && matchesStatus && matchesServer; }) .sort((a: InstalledScript, b: InstalledScript) => { let aValue: any; let bValue: any; switch (sortField) { case 'script_name': aValue = a.script_name.toLowerCase(); bValue = b.script_name.toLowerCase(); break; case 'container_id': aValue = a.container_id ?? ''; bValue = b.container_id ?? ''; break; case 'server_name': aValue = a.server_name ?? 'Local'; bValue = b.server_name ?? 'Local'; break; case 'status': aValue = a.status; bValue = b.status; break; case 'installation_date': aValue = new Date(a.installation_date).getTime(); bValue = new Date(b.installation_date).getTime(); break; default: return 0; } if (aValue < bValue) { return sortDirection === 'asc' ? -1 : 1; } if (aValue > bValue) { return sortDirection === 'asc' ? 1 : -1; } return 0; }); // Get unique servers for filter const uniqueServers: string[] = []; const seen = new Set(); for (const script of scripts) { if (script.server_name && !seen.has(String(script.server_name))) { uniqueServers.push(String(script.server_name)); seen.add(String(script.server_name)); } } const handleDeleteScript = (id: number) => { if (confirm('Are you sure you want to delete this installation record?')) { void deleteScriptMutation.mutate({ id }); } }; const handleUpdateScript = (script: InstalledScript) => { if (!script.container_id) { alert('No Container ID available for this script'); return; } if (confirm(`Are you sure you want to update ${script.script_name}?`)) { // Get server info if it's SSH mode let server = null; if (script.server_id && script.server_user && script.server_password) { server = { id: script.server_id, name: script.server_name, ip: script.server_ip, user: script.server_user, password: script.server_password }; } setUpdatingScript({ id: script.id, containerId: script.container_id, server: server }); } }; const handleCloseUpdateTerminal = () => { setUpdatingScript(null); }; const handleEditScript = (script: InstalledScript) => { setEditingScriptId(script.id); setEditFormData({ script_name: script.script_name, container_id: script.container_id ?? '' }); }; const handleCancelEdit = () => { setEditingScriptId(null); setEditFormData({ script_name: '', container_id: '' }); }; const handleSaveEdit = () => { if (!editFormData.script_name.trim()) { alert('Script name is required'); return; } if (editingScriptId) { updateScriptMutation.mutate({ id: editingScriptId, script_name: editFormData.script_name.trim(), container_id: editFormData.container_id.trim() || undefined, }); } }; const handleInputChange = (field: 'script_name' | 'container_id', value: string) => { setEditFormData(prev => ({ ...prev, [field]: value })); }; const handleAddFormChange = (field: 'script_name' | 'container_id' | 'server_id', value: string) => { setAddFormData(prev => ({ ...prev, [field]: value })); }; const handleAddScript = () => { if (!addFormData.script_name.trim()) { alert('Script name is required'); return; } createScriptMutation.mutate({ script_name: addFormData.script_name.trim(), script_path: `manual/${addFormData.script_name.trim()}`, container_id: addFormData.container_id.trim() || undefined, server_id: addFormData.server_id === 'local' ? undefined : Number(addFormData.server_id), execution_mode: addFormData.server_id === 'local' ? 'local' : 'ssh', status: 'success' }); }; const handleCancelAdd = () => { setShowAddForm(false); setAddFormData({ script_name: '', container_id: '', server_id: 'local' }); }; const handleAutoDetect = () => { if (!autoDetectServerId) { return; } if (autoDetectMutation.isPending) { return; } setAutoDetectStatus({ type: null, message: '' }); console.log('Starting auto-detect for server ID:', autoDetectServerId); autoDetectMutation.mutate({ serverId: Number(autoDetectServerId) }); }; const handleCancelAutoDetect = () => { setShowAutoDetectForm(false); setAutoDetectServerId(''); }; const handleSort = (field: 'script_name' | 'container_id' | 'server_name' | 'status' | 'installation_date') => { if (sortField === field) { setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc'); } else { setSortField(field); setSortDirection('asc'); } }; const formatDate = (dateString: string) => { return new Date(dateString).toLocaleString(); }; if (isLoading) { return (
Loading installed scripts...
); } return (
{/* Update Terminal */} {updatingScript && (
)} {/* Header with Stats */}

Installed Scripts

{stats && (
{stats.total}
Total Installations
{stats.byStatus.success}
Successful
{stats.byStatus.failed}
Failed
{stats.byStatus.in_progress}
In Progress
)} {/* Add Script and Auto-Detect Buttons */}
{/* Add Script Form */} {showAddForm && (

Add Manual Script Entry

handleAddFormChange('script_name', e.target.value)} className="w-full px-3 py-2 border border-input rounded-md bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring" placeholder="Enter script name" />
handleAddFormChange('container_id', e.target.value)} className="w-full px-3 py-2 border border-input rounded-md bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring" placeholder="Enter container ID" />
)} {/* Status Messages */} {(autoDetectStatus.type ?? cleanupStatus.type) && (
{/* Auto-Detect Status Message */} {autoDetectStatus.type && (
{autoDetectStatus.type === 'success' ? ( ) : ( )}

{autoDetectStatus.message}

)} {/* Cleanup Status Message */} {cleanupStatus.type && (
{cleanupStatus.type === 'success' ? ( ) : ( )}

{cleanupStatus.message}

)}
)} {/* Auto-Detect LXC Containers Form */} {showAutoDetectForm && (

Auto-Detect LXC Containers (Must contain a tag with "community-script")

How it works

This feature will:

  • Connect to the selected server via SSH
  • Scan all LXC config files in /etc/pve/lxc/
  • Find containers with "community-script" in their tags
  • Extract the container ID and hostname
  • Add them as installed script entries
)} {/* Filters */}
{/* Search Input - Full Width on Mobile */}
setSearchTerm(e.target.value)} className="w-full px-3 py-2 border border-border rounded-md bg-card text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring" />
{/* Filter Dropdowns - Responsive Grid */}
{/* Scripts Display - Mobile Cards / Desktop Table */}
{filteredScripts.length === 0 ? (
{scripts.length === 0 ? 'No installed scripts found.' : 'No scripts match your filters.'}
) : ( <> {/* Mobile Card Layout */}
{filteredScripts.map((script) => ( handleEditScript(script)} onSave={handleSaveEdit} onCancel={handleCancelEdit} onUpdate={() => handleUpdateScript(script)} onDelete={() => handleDeleteScript(Number(script.id))} isUpdating={updateScriptMutation.isPending} isDeleting={deleteScriptMutation.isPending} /> ))}
{/* Desktop Table Layout */}
{filteredScripts.map((script) => ( ))}
handleSort('script_name')} >
Script Name {sortField === 'script_name' && ( {sortDirection === 'asc' ? '↑' : '↓'} )}
handleSort('container_id')} >
Container ID {sortField === 'container_id' && ( {sortDirection === 'asc' ? '↑' : '↓'} )}
handleSort('server_name')} >
Server {sortField === 'server_name' && ( {sortDirection === 'asc' ? '↑' : '↓'} )}
handleSort('status')} >
Status {sortField === 'status' && ( {sortDirection === 'asc' ? '↑' : '↓'} )}
handleSort('installation_date')} >
Installation Date {sortField === 'installation_date' && ( {sortDirection === 'asc' ? '↑' : '↓'} )}
Actions
{editingScriptId === script.id ? (
handleInputChange('script_name', e.target.value)} className="w-full px-2 py-1 text-sm border border-border rounded bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary" placeholder="Script name" />
{script.script_path}
) : (
{script.script_name}
{script.script_path}
)}
{editingScriptId === script.id ? ( handleInputChange('container_id', e.target.value)} className="w-full px-2 py-1 text-sm font-mono border border-border rounded bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary" placeholder="Container ID" /> ) : ( script.container_id ? ( {String(script.container_id)} ) : ( - ) )} {script.server_name ?? '-'} {script.status.replace('_', ' ').toUpperCase()} {formatDate(String(script.installation_date))}
{editingScriptId === script.id ? ( <> ) : ( <> {script.container_id && ( )} )}
)}
); }