diff --git a/src/app/_components/InstalledScriptsTab.tsx b/src/app/_components/InstalledScriptsTab.tsx index ef1f115..9bf501d 100644 --- a/src/app/_components/InstalledScriptsTab.tsx +++ b/src/app/_components/InstalledScriptsTab.tsx @@ -1,27 +1,27 @@ -'use client'; +"use client"; -import { useState, useEffect, useRef, useMemo } 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 { ConfirmationModal } from './ConfirmationModal'; -import { ErrorModal } from './ErrorModal'; -import { LoadingModal } from './LoadingModal'; -import { LXCSettingsModal } from './LXCSettingsModal'; -import { StorageSelectionModal } from './StorageSelectionModal'; -import { BackupWarningModal } from './BackupWarningModal'; -import type { Storage } from '~/server/services/storageService'; -import { getContrastColor } from '../../lib/colorUtils'; +import { useState, useEffect, useRef, useMemo } 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 { ConfirmationModal } from "./ConfirmationModal"; +import { ErrorModal } from "./ErrorModal"; +import { LoadingModal } from "./LoadingModal"; +import { LXCSettingsModal } from "./LXCSettingsModal"; +import { StorageSelectionModal } from "./StorageSelectionModal"; +import { BackupWarningModal } from "./BackupWarningModal"; +import type { Storage } from "~/server/services/storageService"; +import { getContrastColor } from "../../lib/colorUtils"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator, -} from './ui/dropdown-menu'; -import { Settings } from 'lucide-react'; +} from "./ui/dropdown-menu"; +import { Settings } from "lucide-react"; interface InstalledScript { id: number; @@ -39,45 +39,81 @@ interface InstalledScript { server_ssh_port: number | null; server_color: string | null; installation_date: string; - status: 'in_progress' | 'success' | 'failed'; + status: "in_progress" | "success" | "failed"; output_log: string | null; - execution_mode: 'local' | 'ssh'; - container_status?: 'running' | 'stopped' | 'unknown'; + execution_mode: "local" | "ssh"; + container_status?: "running" | "stopped" | "unknown"; web_ui_ip: string | null; web_ui_port: number | null; is_vm?: boolean; } 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'>('server_name'); - const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); - const [updatingScript, setUpdatingScript] = useState<{ id: number; containerId: string; server?: any; backupStorage?: string; isBackupOnly?: boolean } | null>(null); - const [openingShell, setOpeningShell] = useState<{ id: number; containerId: string; server?: any } | null>(null); + 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" + >("server_name"); + const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); + const [updatingScript, setUpdatingScript] = useState<{ + id: number; + containerId: string; + server?: any; + backupStorage?: string; + isBackupOnly?: boolean; + } | null>(null); + const [openingShell, setOpeningShell] = useState<{ + id: number; + containerId: string; + server?: any; + } | null>(null); const [showBackupPrompt, setShowBackupPrompt] = useState(false); const [showStorageSelection, setShowStorageSelection] = useState(false); - const [pendingUpdateScript, setPendingUpdateScript] = useState(null); + const [pendingUpdateScript, setPendingUpdateScript] = + useState(null); const [backupStorages, setBackupStorages] = useState([]); const [isLoadingStorages, setIsLoadingStorages] = useState(false); const [showBackupWarning, setShowBackupWarning] = useState(false); const [isPreUpdateBackup, setIsPreUpdateBackup] = useState(false); // Track if storage selection is for pre-update backup const [editingScriptId, setEditingScriptId] = useState(null); - const [editFormData, setEditFormData] = useState<{ script_name: string; container_id: string; web_ui_ip: string; web_ui_port: string }>({ script_name: '', container_id: '', web_ui_ip: '', web_ui_port: '' }); + const [editFormData, setEditFormData] = useState<{ + script_name: string; + container_id: string; + web_ui_ip: string; + web_ui_port: string; + }>({ script_name: "", container_id: "", web_ui_ip: "", web_ui_port: "" }); 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 [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 [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); // Container control state - const [containerStatuses, setContainerStatuses] = useState>(new Map()); + const [containerStatuses, setContainerStatuses] = useState< + Map + >(new Map()); const [confirmationModal, setConfirmationModal] = useState<{ isOpen: boolean; - variant: 'simple' | 'danger'; + variant: "simple" | "danger"; title: string; message: string; confirmText?: string; @@ -85,17 +121,19 @@ export function InstalledScriptsTab() { cancelButtonText?: string; onConfirm: () => void; } | null>(null); - const [controllingScriptId, setControllingScriptId] = useState(null); + const [controllingScriptId, setControllingScriptId] = useState( + null, + ); const scriptsRef = useRef([]); const statusCheckTimeoutRef = useRef(null); - + // Error modal state const [errorModal, setErrorModal] = useState<{ isOpen: boolean; title: string; message: string; details?: string; - type?: 'error' | 'success'; + type?: "error" | "success"; } | null>(null); // Loading modal state @@ -111,192 +149,242 @@ export function InstalledScriptsTab() { }>({ isOpen: false, script: null }); // Fetch installed scripts - const { data: scriptsData, refetch: refetchScripts, isLoading } = api.installedScripts.getAllInstalledScripts.useQuery(); - const { data: statsData } = api.installedScripts.getInstallationStats.useQuery(); + 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(); - } - }); + 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: '', web_ui_ip: '', web_ui_port: '' }); - }, - onError: (error) => { - alert(`Error updating script: ${error.message}`); - } - }); + const updateScriptMutation = + api.installedScripts.updateInstalledScript.useMutation({ + onSuccess: () => { + void refetchScripts(); + setEditingScriptId(null); + setEditFormData({ + script_name: "", + container_id: "", + web_ui_ip: "", + web_ui_port: "", + }); + }, + 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}`); - } - }); + 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) => { - 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); - } - }); + const autoDetectMutation = + api.installedScripts.autoDetectLXCContainers.useMutation({ + onSuccess: (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, + ); + }, + }); // Get container statuses mutation - const containerStatusMutation = api.installedScripts.getContainerStatuses.useMutation({ - onSuccess: (data) => { - if (data.success) { - - // Map container IDs to script IDs - const currentScripts = scriptsRef.current; - const statusMap = new Map(); - - // For each script, find its container status - currentScripts.forEach(script => { - if (script.container_id && data.statusMap) { - const containerStatus = (data.statusMap as Record)[script.container_id]; - if (containerStatus) { - statusMap.set(script.id, containerStatus); + const containerStatusMutation = + api.installedScripts.getContainerStatuses.useMutation({ + onSuccess: (data) => { + if (data.success) { + // Map container IDs to script IDs + const currentScripts = scriptsRef.current; + const statusMap = new Map< + number, + "running" | "stopped" | "unknown" + >(); + + // For each script, find its container status + currentScripts.forEach((script) => { + if (script.container_id && data.statusMap) { + const containerStatus = ( + data.statusMap as Record< + string, + "running" | "stopped" | "unknown" + > + )[script.container_id]; + if (containerStatus) { + statusMap.set(script.id, containerStatus); + } else { + statusMap.set(script.id, "unknown"); + } } else { - statusMap.set(script.id, 'unknown'); + statusMap.set(script.id, "unknown"); } - } else { - statusMap.set(script.id, 'unknown'); - } - }); - - setContainerStatuses(statusMap); - } else { - console.error('Container status fetch failed:', data.error); - } - }, - onError: (error) => { - console.error('Error fetching container statuses:', error); - } - }); + }); + + setContainerStatuses(statusMap); + } else { + console.error("Container status fetch failed:", data.error); + } + }, + onError: (error) => { + console.error("Error fetching container statuses:", error); + }, + }); // Ref for container status mutation to avoid dependency loops const containerStatusMutationRef = useRef(containerStatusMutation); // Cleanup orphaned scripts mutation - const cleanupMutation = api.installedScripts.cleanupOrphanedScripts.useMutation({ - onSuccess: (data) => { - void refetchScripts(); - - if (data.deletedCount > 0) { - setCleanupStatus({ - type: 'success', - message: `Cleanup completed! Removed ${data.deletedCount} orphaned script(s): ${data.deletedScripts.join(', ')}` + const cleanupMutation = + api.installedScripts.cleanupOrphanedScripts.useMutation({ + onSuccess: (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.", }); - } 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: '' }), 8000); - } - }); + // Clear status after 5 seconds + setTimeout(() => setCleanupStatus({ type: null, message: "" }), 8000); + }, + }); // Auto-detect Web UI mutation - const autoDetectWebUIMutation = api.installedScripts.autoDetectWebUI.useMutation({ - onSuccess: (data) => { - console.log('✅ Auto-detect WebUI success:', data); - void refetchScripts(); - setAutoDetectStatus({ - type: 'success', - message: data.success ? `Detected IP: ${data.ip}` : (data.error ?? 'Failed to detect Web UI') - }); - setTimeout(() => setAutoDetectStatus({ type: null, message: '' }), 5000); - }, - onError: (error) => { - console.error('❌ Auto-detect WebUI error:', error); - setAutoDetectStatus({ - type: 'error', - message: error.message ?? 'Failed to detect Web UI' - }); - setTimeout(() => setAutoDetectStatus({ type: null, message: '' }), 8000); - } - }); + const autoDetectWebUIMutation = + api.installedScripts.autoDetectWebUI.useMutation({ + onSuccess: (data) => { + console.log("✅ Auto-detect WebUI success:", data); + void refetchScripts(); + setAutoDetectStatus({ + type: "success", + message: data.success + ? `Detected IP: ${data.ip}` + : (data.error ?? "Failed to detect Web UI"), + }); + setTimeout( + () => setAutoDetectStatus({ type: null, message: "" }), + 5000, + ); + }, + onError: (error) => { + console.error("❌ Auto-detect WebUI error:", error); + setAutoDetectStatus({ + type: "error", + message: error.message ?? "Failed to detect Web UI", + }); + setTimeout( + () => setAutoDetectStatus({ type: null, message: "" }), + 8000, + ); + }, + }); // Get backup storages query - const getBackupStoragesQuery = api.installedScripts.getBackupStorages.useQuery( - { serverId: pendingUpdateScript?.server_id ?? 0, forceRefresh: false }, - { enabled: false } // Only fetch when explicitly called - ); + const getBackupStoragesQuery = + api.installedScripts.getBackupStorages.useQuery( + { serverId: pendingUpdateScript?.server_id ?? 0, forceRefresh: false }, + { enabled: false }, // Only fetch when explicitly called + ); const fetchStorages = async (serverId: number, forceRefresh = false) => { setIsLoadingStorages(true); try { - const result = await getBackupStoragesQuery.refetch({ - queryKey: ['installedScripts.getBackupStorages', { serverId, forceRefresh }] + const result = await getBackupStoragesQuery.refetch({ + queryKey: [ + "installedScripts.getBackupStorages", + { serverId, forceRefresh }, + ], }); if (result.data?.success) { setBackupStorages(result.data.storages); } else { setErrorModal({ isOpen: true, - title: 'Failed to Fetch Storages', - message: result.data?.error ?? 'Unknown error occurred', - type: 'error' + title: "Failed to Fetch Storages", + message: result.data?.error ?? "Unknown error occurred", + type: "error", }); } } catch (error) { setErrorModal({ isOpen: true, - title: 'Failed to Fetch Storages', - message: error instanceof Error ? error.message : 'Unknown error occurred', - type: 'error' + title: "Failed to Fetch Storages", + message: + error instanceof Error ? error.message : "Unknown error occurred", + type: "error", }); } finally { setIsLoadingStorages(false); @@ -306,159 +394,181 @@ export function InstalledScriptsTab() { // Container control mutations // Note: getStatusMutation removed - using direct API calls instead - const controlContainerMutation = api.installedScripts.controlContainer.useMutation({ - onSuccess: (data, variables) => { - setLoadingModal(null); - setControllingScriptId(null); - - if (data.success) { - // Update container status immediately in UI for instant feedback - const newStatus = variables.action === 'start' ? 'running' : 'stopped'; - setContainerStatuses(prev => { - const newMap = new Map(prev); - // Find the script ID for this container using the container ID from the response - const currentScripts = scriptsRef.current; - const script = currentScripts.find(s => s.container_id === data.containerId); - if (script) { - newMap.set(script.id, newStatus); - } - return newMap; - }); + const controlContainerMutation = + api.installedScripts.controlContainer.useMutation({ + onSuccess: (data, variables) => { + setLoadingModal(null); + setControllingScriptId(null); - // Show success modal + if (data.success) { + // Update container status immediately in UI for instant feedback + const newStatus = + variables.action === "start" ? "running" : "stopped"; + setContainerStatuses((prev) => { + const newMap = new Map(prev); + // Find the script ID for this container using the container ID from the response + const currentScripts = scriptsRef.current; + const script = currentScripts.find( + (s) => s.container_id === data.containerId, + ); + if (script) { + newMap.set(script.id, newStatus); + } + return newMap; + }); + + // Show success modal + setErrorModal({ + isOpen: true, + title: `Container ${variables.action === "start" ? "Started" : "Stopped"}`, + message: + data.message ?? + `Container has been ${variables.action === "start" ? "started" : "stopped"} successfully.`, + details: undefined, + type: "success", + }); + + // Re-fetch status for all containers using bulk method (in background) + // Trigger status check by updating scripts length dependency + // This will be handled by the useEffect that watches scripts.length + } else { + // Show error message from backend + const errorMessage = data.error ?? "Unknown error occurred"; + setErrorModal({ + isOpen: true, + title: "Container Control Failed", + message: + "Failed to control the container. Please check the error details below.", + details: errorMessage, + }); + } + }, + onError: (error) => { + console.error("Container control error:", error); + setLoadingModal(null); + setControllingScriptId(null); + + // Show detailed error message + const errorMessage = error.message ?? "Unknown error occurred"; setErrorModal({ isOpen: true, - title: `Container ${variables.action === 'start' ? 'Started' : 'Stopped'}`, - message: data.message ?? `Container has been ${variables.action === 'start' ? 'started' : 'stopped'} successfully.`, - details: undefined, - type: 'success' + title: "Container Control Failed", + message: + "An unexpected error occurred while controlling the container.", + details: errorMessage, }); + }, + }); - // Re-fetch status for all containers using bulk method (in background) - // Trigger status check by updating scripts length dependency - // This will be handled by the useEffect that watches scripts.length - } else { - // Show error message from backend - const errorMessage = data.error ?? 'Unknown error occurred'; + const destroyContainerMutation = + api.installedScripts.destroyContainer.useMutation({ + onSuccess: (data) => { + setLoadingModal(null); + setControllingScriptId(null); + + if (data.success) { + void refetchScripts(); + setErrorModal({ + isOpen: true, + title: "Container Destroyed", + message: + data.message ?? + "The container has been successfully destroyed and removed from the database.", + details: undefined, + type: "success", + }); + } else { + // Show error message from backend + const errorMessage = data.error ?? "Unknown error occurred"; + setErrorModal({ + isOpen: true, + title: "Container Destroy Failed", + message: + "Failed to destroy the container. Please check the error details below.", + details: errorMessage, + }); + } + }, + onError: (error) => { + console.error("Container destroy error:", error); + setLoadingModal(null); + setControllingScriptId(null); + + // Show detailed error message + const errorMessage = error.message ?? "Unknown error occurred"; setErrorModal({ isOpen: true, - title: 'Container Control Failed', - message: 'Failed to control the container. Please check the error details below.', - details: errorMessage + title: "Container Destroy Failed", + message: + "An unexpected error occurred while destroying the container.", + details: errorMessage, }); - } - }, - onError: (error) => { - console.error('Container control error:', error); - setLoadingModal(null); - setControllingScriptId(null); - - // Show detailed error message - const errorMessage = error.message ?? 'Unknown error occurred'; - setErrorModal({ - isOpen: true, - title: 'Container Control Failed', - message: 'An unexpected error occurred while controlling the container.', - details: errorMessage - }); - } - }); + }, + }); - const destroyContainerMutation = api.installedScripts.destroyContainer.useMutation({ - onSuccess: (data) => { - setLoadingModal(null); - setControllingScriptId(null); - - if (data.success) { - void refetchScripts(); - setErrorModal({ - isOpen: true, - title: 'Container Destroyed', - message: data.message ?? 'The container has been successfully destroyed and removed from the database.', - details: undefined, - type: 'success' - }); - } else { - // Show error message from backend - const errorMessage = data.error ?? 'Unknown error occurred'; - setErrorModal({ - isOpen: true, - title: 'Container Destroy Failed', - message: 'Failed to destroy the container. Please check the error details below.', - details: errorMessage - }); - } - }, - onError: (error) => { - console.error('Container destroy error:', error); - setLoadingModal(null); - setControllingScriptId(null); - - // Show detailed error message - const errorMessage = error.message ?? 'Unknown error occurred'; - setErrorModal({ - isOpen: true, - title: 'Container Destroy Failed', - message: 'An unexpected error occurred while destroying the container.', - details: errorMessage - }); - } - }); - - - const scripts: InstalledScript[] = useMemo(() => (scriptsData?.scripts as InstalledScript[]) ?? [], [scriptsData?.scripts]); + const scripts: InstalledScript[] = useMemo( + () => (scriptsData?.scripts as InstalledScript[]) ?? [], + [scriptsData?.scripts], + ); const stats = statsData?.stats; // Update refs when data changes useEffect(() => { scriptsRef.current = scripts; }, [scripts]); - + useEffect(() => { containerStatusMutationRef.current = containerStatusMutation; }, [containerStatusMutation]); - // Run cleanup when component mounts and scripts are loaded (only once) useEffect(() => { - if (scripts.length > 0 && serversData?.servers && !cleanupMutation.isPending && !cleanupRunRef.current) { + if ( + scripts.length > 0 && + serversData?.servers && + !cleanupMutation.isPending && + !cleanupRunRef.current + ) { cleanupRunRef.current = true; void cleanupMutation.mutate(); } }, [scripts.length, serversData?.servers, cleanupMutation]); - useEffect(() => { if (scripts.length > 0) { - console.log('Status check triggered - scripts length:', scripts.length); - + console.log("Status check triggered - scripts length:", scripts.length); + // Clear any existing timeout if (statusCheckTimeoutRef.current) { clearTimeout(statusCheckTimeoutRef.current); } - + // Debounce status checks by 500ms statusCheckTimeoutRef.current = setTimeout(() => { // Prevent multiple simultaneous status checks if (containerStatusMutationRef.current.isPending) { - console.log('Status check already pending, skipping'); + console.log("Status check already pending, skipping"); return; } - - const currentScripts = scriptsRef.current; - - // Get unique server IDs from scripts - const serverIds = [...new Set(currentScripts - .filter(script => script.server_id) - .map(script => script.server_id!))]; - console.log('Executing status check for server IDs:', serverIds); + const currentScripts = scriptsRef.current; + + // Get unique server IDs from scripts + const serverIds = [ + ...new Set( + currentScripts + .filter((script) => script.server_id) + .map((script) => script.server_id!), + ), + ]; + + console.log("Executing status check for server IDs:", serverIds); if (serverIds.length > 0) { containerStatusMutationRef.current.mutate({ serverIds }); } }, 500); } - }, [scripts.length]); + }, [scripts.length]); // Cleanup timeout on unmount useEffect(() => { @@ -469,50 +579,56 @@ export function InstalledScriptsTab() { }; }, []); - const scriptsWithStatus = scripts.map(script => ({ + const scriptsWithStatus = scripts.map((script) => ({ ...script, - container_status: script.container_id ? containerStatuses.get(script.id) ?? 'unknown' : undefined + container_status: script.container_id + ? (containerStatuses.get(script.id) ?? "unknown") + : undefined, })); // Filter and sort scripts const filteredScripts = scriptsWithStatus .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); - + 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) => { // Default sorting: group by server, then by container ID - if (sortField === 'server_name') { - const aServer = a.server_name ?? 'Local'; - const bServer = b.server_name ?? 'Local'; - + if (sortField === "server_name") { + const aServer = a.server_name ?? "Local"; + const bServer = b.server_name ?? "Local"; + // First sort by server name if (aServer !== bServer) { - return sortDirection === 'asc' ? - aServer.localeCompare(bServer) : - bServer.localeCompare(aServer); + return sortDirection === "asc" + ? aServer.localeCompare(bServer) + : bServer.localeCompare(aServer); } - + // If same server, sort by container ID - const aContainerId = a.container_id ?? ''; - const bContainerId = b.container_id ?? ''; - + const aContainerId = a.container_id ?? ""; + const bContainerId = b.container_id ?? ""; + if (aContainerId !== bContainerId) { // Convert to numbers for proper numeric sorting const aNum = parseInt(aContainerId) || 0; const bNum = parseInt(bContainerId) || 0; - return sortDirection === 'asc' ? aNum - bNum : bNum - aNum; + return sortDirection === "asc" ? aNum - bNum : bNum - aNum; } - + return 0; } @@ -521,19 +637,19 @@ export function InstalledScriptsTab() { let bValue: any; switch (sortField) { - case 'script_name': + 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 ?? ''; + case "container_id": + aValue = a.container_id ?? ""; + bValue = b.container_id ?? ""; break; - case 'status': + case "status": aValue = a.status; bValue = b.status; break; - case 'installation_date': + case "installation_date": aValue = new Date(a.installation_date).getTime(); bValue = new Date(b.installation_date).getTime(); break; @@ -542,10 +658,10 @@ export function InstalledScriptsTab() { } if (aValue < bValue) { - return sortDirection === 'asc' ? -1 : 1; + return sortDirection === "asc" ? -1 : 1; } if (aValue > bValue) { - return sortDirection === 'asc' ? 1 : -1; + return sortDirection === "asc" ? 1 : -1; } return 0; }); @@ -561,67 +677,81 @@ export function InstalledScriptsTab() { } const handleDeleteScript = (id: number, script?: InstalledScript) => { - const scriptToDelete = script ?? scripts.find(s => s.id === id); - - if (scriptToDelete && scriptToDelete.container_id && scriptToDelete.execution_mode === 'ssh') { + const scriptToDelete = script ?? scripts.find((s) => s.id === id); + + if ( + scriptToDelete?.container_id && + scriptToDelete.execution_mode === "ssh" + ) { // For SSH scripts with container_id, use confirmation modal setConfirmationModal({ isOpen: true, - variant: 'simple', - title: 'Delete Database Record Only', + variant: "simple", + title: "Delete Database Record Only", message: `This will only delete the database record for "${scriptToDelete.script_name}" (Container ID: ${scriptToDelete.container_id}).\n\nThe container will remain intact and can be re-detected later via auto-detect.`, onConfirm: () => { void deleteScriptMutation.mutate({ id }); setConfirmationModal(null); - } + }, }); } else { // For non-SSH scripts or scripts without container_id, use simple confirm - if (confirm('Are you sure you want to delete this installation record?')) { + if ( + confirm("Are you sure you want to delete this installation record?") + ) { void deleteScriptMutation.mutate({ id }); } } }; // Container control handlers - const handleStartStop = (script: InstalledScript, action: 'start' | 'stop') => { + const handleStartStop = ( + script: InstalledScript, + action: "start" | "stop", + ) => { if (!script.container_id) { - alert('No Container ID available for this script'); + alert("No Container ID available for this script"); return; } setConfirmationModal({ isOpen: true, - variant: 'simple', - title: `${action === 'start' ? 'Start' : 'Stop'} Container`, + variant: "simple", + title: `${action === "start" ? "Start" : "Stop"} Container`, message: `Are you sure you want to ${action} container ${script.container_id} (${script.script_name})?`, onConfirm: () => { setControllingScriptId(script.id); - setLoadingModal({ isOpen: true, action: `${action === 'start' ? 'Starting' : 'Stopping'} container ${script.container_id}...` }); + setLoadingModal({ + isOpen: true, + action: `${action === "start" ? "Starting" : "Stopping"} container ${script.container_id}...`, + }); void controlContainerMutation.mutate({ id: script.id, action }); setConfirmationModal(null); - } + }, }); }; const handleDestroy = (script: InstalledScript) => { if (!script.container_id) { - alert('No Container ID available for this script'); + alert("No Container ID available for this script"); return; } setConfirmationModal({ isOpen: true, - variant: 'danger', - title: 'Destroy Container', + variant: "danger", + title: "Destroy Container", message: `This will permanently destroy the LXC container ${script.container_id} (${script.script_name}) and all its data. This action cannot be undone!`, confirmText: script.container_id, onConfirm: () => { setControllingScriptId(script.id); - setLoadingModal({ isOpen: true, action: `Destroying container ${script.container_id}...` }); + setLoadingModal({ + isOpen: true, + action: `Destroying container ${script.container_id}...`, + }); void destroyContainerMutation.mutate({ id: script.id }); setConfirmationModal(null); - } + }, }); }; @@ -629,34 +759,35 @@ export function InstalledScriptsTab() { if (!script.container_id) { setErrorModal({ isOpen: true, - title: 'Update Failed', - message: 'No Container ID available for this script', - details: 'This script does not have a valid container ID and cannot be updated.' + title: "Update Failed", + message: "No Container ID available for this script", + details: + "This script does not have a valid container ID and cannot be updated.", }); return; } - + // Show confirmation modal with type-to-confirm for update setConfirmationModal({ isOpen: true, - title: 'Confirm Script Update', + title: "Confirm Script Update", message: `Are you sure you want to update "${script.script_name}"?\n\n⚠️ WARNING: This will update the script and may affect the container. Consider backing up your data beforehand.`, - variant: 'danger', + variant: "danger", confirmText: script.container_id, - confirmButtonText: 'Continue', + confirmButtonText: "Continue", onConfirm: () => { setConfirmationModal(null); // Store the script for backup flow setPendingUpdateScript(script); // Show backup prompt setShowBackupPrompt(true); - } + }, }); }; const handleBackupPromptResponse = (wantsBackup: boolean) => { setShowBackupPrompt(false); - + if (!pendingUpdateScript) return; if (wantsBackup) { @@ -668,9 +799,10 @@ export function InstalledScriptsTab() { } else { setErrorModal({ isOpen: true, - title: 'Backup Not Available', - message: 'Backup is only available for SSH scripts with a configured server.', - type: 'error' + title: "Backup Not Available", + message: + "Backup is only available for SSH scripts with a configured server.", + type: "error", }); // Proceed without backup proceedWithUpdate(null); @@ -683,7 +815,7 @@ export function InstalledScriptsTab() { const handleStorageSelected = (storage: Storage) => { setShowStorageSelection(false); - + // Check if this is for a standalone backup or pre-update backup if (isPreUpdateBackup) { // Pre-update backup - proceed with update @@ -695,7 +827,10 @@ export function InstalledScriptsTab() { } }; - const executeStandaloneBackup = (script: InstalledScript, storageName: string) => { + const executeStandaloneBackup = ( + script: InstalledScript, + storageName: string, + ) => { // Get server info let server = null; if (script.server_id && script.server_user) { @@ -705,10 +840,10 @@ export function InstalledScriptsTab() { ip: script.server_ip, user: script.server_user, password: script.server_password, - auth_type: script.server_auth_type ?? 'password', + auth_type: script.server_auth_type ?? "password", ssh_key: script.server_ssh_key, ssh_key_passphrase: script.server_ssh_key_passphrase, - ssh_port: script.server_ssh_port ?? 22 + ssh_port: script.server_ssh_port ?? 22, }; } @@ -718,7 +853,7 @@ export function InstalledScriptsTab() { containerId: script.container_id!, server: server, backupStorage: storageName, - isBackupOnly: true + isBackupOnly: true, }); // Reset state @@ -739,21 +874,21 @@ export function InstalledScriptsTab() { ip: pendingUpdateScript.server_ip, user: pendingUpdateScript.server_user, password: pendingUpdateScript.server_password, - auth_type: pendingUpdateScript.server_auth_type ?? 'password', + auth_type: pendingUpdateScript.server_auth_type ?? "password", ssh_key: pendingUpdateScript.server_ssh_key, ssh_key_passphrase: pendingUpdateScript.server_ssh_key_passphrase, - ssh_port: pendingUpdateScript.server_ssh_port ?? 22 + ssh_port: pendingUpdateScript.server_ssh_port ?? 22, }; } - + setUpdatingScript({ id: pendingUpdateScript.id, containerId: pendingUpdateScript.container_id!, server: server, backupStorage: backupStorage ?? undefined, - isBackupOnly: false // Explicitly set to false for update operations + isBackupOnly: false, // Explicitly set to false for update operations }); - + // Reset state setPendingUpdateScript(null); setBackupStorages([]); @@ -767,9 +902,10 @@ export function InstalledScriptsTab() { if (!script.container_id) { setErrorModal({ isOpen: true, - title: 'Backup Failed', - message: 'No Container ID available for this script', - details: 'This script does not have a valid container ID and cannot be backed up.' + title: "Backup Failed", + message: "No Container ID available for this script", + details: + "This script does not have a valid container ID and cannot be backed up.", }); return; } @@ -777,9 +913,10 @@ export function InstalledScriptsTab() { if (!script.server_id) { setErrorModal({ isOpen: true, - title: 'Backup Not Available', - message: 'Backup is only available for SSH scripts with a configured server.', - type: 'error' + title: "Backup Not Available", + message: + "Backup is only available for SSH scripts with a configured server.", + type: "error", }); return; } @@ -795,13 +932,14 @@ export function InstalledScriptsTab() { if (!script.container_id) { setErrorModal({ isOpen: true, - title: 'Shell Access Failed', - message: 'No Container ID available for this script', - details: 'This script does not have a valid container ID and cannot be accessed via shell.' + title: "Shell Access Failed", + message: "No Container ID available for this script", + details: + "This script does not have a valid container ID and cannot be accessed via shell.", }); return; } - + // Get server info if it's SSH mode let server = null; if (script.server_id && script.server_user) { @@ -811,17 +949,17 @@ export function InstalledScriptsTab() { ip: script.server_ip, user: script.server_user, password: script.server_password, - auth_type: script.server_auth_type ?? 'password', + auth_type: script.server_auth_type ?? "password", ssh_key: script.server_ssh_key, ssh_key_passphrase: script.server_ssh_key_passphrase, - ssh_port: script.server_ssh_port ?? 22 + ssh_port: script.server_ssh_port ?? 22, }; } - + setOpeningShell({ id: script.id, containerId: script.container_id, - server: server + server: server, }); }; @@ -834,19 +972,21 @@ export function InstalledScriptsTab() { if (openingShell) { // Small delay to ensure the terminal is rendered setTimeout(() => { - const terminalElement = document.querySelector('[data-terminal="shell"]'); + const terminalElement = document.querySelector( + '[data-terminal="shell"]', + ); if (terminalElement) { // Scroll to the terminal with smooth animation - terminalElement.scrollIntoView({ - behavior: 'smooth', - block: 'start', - inline: 'nearest' + terminalElement.scrollIntoView({ + behavior: "smooth", + block: "start", + inline: "nearest", }); - + // Add a subtle highlight effect - terminalElement.classList.add('animate-pulse'); + terminalElement.classList.add("animate-pulse"); setTimeout(() => { - terminalElement.classList.remove('animate-pulse'); + terminalElement.classList.remove("animate-pulse"); }, 2000); } }, 200); @@ -857,19 +997,21 @@ export function InstalledScriptsTab() { if (updatingScript) { // Small delay to ensure the terminal is rendered setTimeout(() => { - const terminalElement = document.querySelector('[data-terminal="update"]'); + const terminalElement = document.querySelector( + '[data-terminal="update"]', + ); if (terminalElement) { // Scroll to the terminal with smooth animation - terminalElement.scrollIntoView({ - behavior: 'smooth', - block: 'start', - inline: 'nearest' + terminalElement.scrollIntoView({ + behavior: "smooth", + block: "start", + inline: "nearest", }); - + // Add a subtle highlight effect - terminalElement.classList.add('animate-pulse'); + terminalElement.classList.add("animate-pulse"); setTimeout(() => { - terminalElement.classList.remove('animate-pulse'); + terminalElement.classList.remove("animate-pulse"); }, 2000); } }, 200); @@ -880,15 +1022,20 @@ export function InstalledScriptsTab() { setEditingScriptId(script.id); setEditFormData({ script_name: script.script_name, - container_id: script.container_id ?? '', - web_ui_ip: script.web_ui_ip ?? '', - web_ui_port: script.web_ui_port?.toString() ?? '' + container_id: script.container_id ?? "", + web_ui_ip: script.web_ui_ip ?? "", + web_ui_port: script.web_ui_port?.toString() ?? "", }); }; const handleCancelEdit = () => { setEditingScriptId(null); - setEditFormData({ script_name: '', container_id: '', web_ui_ip: '', web_ui_port: '' }); + setEditFormData({ + script_name: "", + container_id: "", + web_ui_ip: "", + web_ui_port: "", + }); }; const handleLXCSettings = (script: InstalledScript) => { @@ -899,9 +1046,9 @@ export function InstalledScriptsTab() { if (!editFormData.script_name.trim()) { setErrorModal({ isOpen: true, - title: 'Validation Error', - message: 'Script name is required', - details: 'Please enter a valid script name before saving.' + title: "Validation Error", + message: "Script name is required", + details: "Please enter a valid script name before saving.", }); return; } @@ -912,28 +1059,36 @@ export function InstalledScriptsTab() { script_name: editFormData.script_name.trim(), container_id: editFormData.container_id.trim() || undefined, web_ui_ip: editFormData.web_ui_ip.trim() || undefined, - web_ui_port: editFormData.web_ui_port.trim() ? parseInt(editFormData.web_ui_port, 10) : undefined, + web_ui_port: editFormData.web_ui_port.trim() + ? parseInt(editFormData.web_ui_port, 10) + : undefined, }); } }; - const handleInputChange = (field: 'script_name' | 'container_id' | 'web_ui_ip' | 'web_ui_port', value: string) => { - setEditFormData(prev => ({ + const handleInputChange = ( + field: "script_name" | "container_id" | "web_ui_ip" | "web_ui_port", + value: string, + ) => { + setEditFormData((prev) => ({ ...prev, - [field]: value + [field]: value, })); }; - const handleAddFormChange = (field: 'script_name' | 'container_id' | 'server_id', value: string) => { - setAddFormData(prev => ({ + const handleAddFormChange = ( + field: "script_name" | "container_id" | "server_id", + value: string, + ) => { + setAddFormData((prev) => ({ ...prev, - [field]: value + [field]: value, })); }; const handleAddScript = () => { if (!addFormData.script_name.trim()) { - alert('Script name is required'); + alert("Script name is required"); return; } @@ -941,15 +1096,18 @@ export function InstalledScriptsTab() { 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' + 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' }); + setAddFormData({ script_name: "", container_id: "", server_id: "local" }); }; const handleAutoDetect = () => { @@ -961,45 +1119,57 @@ export function InstalledScriptsTab() { return; } - setAutoDetectStatus({ type: null, message: '' }); + setAutoDetectStatus({ type: null, message: "" }); autoDetectMutation.mutate({ serverId: Number(autoDetectServerId) }); }; const handleCancelAutoDetect = () => { setShowAutoDetectForm(false); - setAutoDetectServerId(''); + setAutoDetectServerId(""); }; - const handleSort = (field: 'script_name' | 'container_id' | 'server_name' | 'status' | 'installation_date') => { + const handleSort = ( + field: + | "script_name" + | "container_id" + | "server_name" + | "status" + | "installation_date", + ) => { if (sortField === field) { - setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc'); + setSortDirection(sortDirection === "asc" ? "desc" : "asc"); } else { setSortField(field); - setSortDirection('asc'); + setSortDirection("asc"); } }; const handleAutoDetectWebUI = (script: InstalledScript) => { - console.log('🔍 Auto-detect WebUI clicked for script:', script); - console.log('Script validation:', { + console.log("🔍 Auto-detect WebUI clicked for script:", script); + console.log("Script validation:", { hasContainerId: !!script.container_id, - isSSHMode: script.execution_mode === 'ssh', + isSSHMode: script.execution_mode === "ssh", containerId: script.container_id, - executionMode: script.execution_mode + executionMode: script.execution_mode, }); - - if (!script.container_id || script.execution_mode !== 'ssh') { - console.log('❌ Auto-detect validation failed'); + + if (!script.container_id || script.execution_mode !== "ssh") { + console.log("❌ Auto-detect validation failed"); setErrorModal({ isOpen: true, - title: 'Auto-Detect Failed', - message: 'Auto-detect only works for SSH mode scripts with container ID', - details: 'This script does not have a valid container ID or is not in SSH mode.' + title: "Auto-Detect Failed", + message: + "Auto-detect only works for SSH mode scripts with container ID", + details: + "This script does not have a valid container ID or is not in SSH mode.", }); return; } - console.log('✅ Calling autoDetectWebUIMutation.mutate with id:', script.id); + console.log( + "✅ Calling autoDetectWebUIMutation.mutate with id:", + script.id, + ); autoDetectWebUIMutation.mutate({ id: script.id }); }; @@ -1007,35 +1177,37 @@ export function InstalledScriptsTab() { if (!script.web_ui_ip) { setErrorModal({ isOpen: true, - title: 'Web UI Access Failed', - message: 'No IP address configured for this script', - details: 'Please set the Web UI IP address before opening the interface.' + title: "Web UI Access Failed", + message: "No IP address configured for this script", + details: + "Please set the Web UI IP address before opening the interface.", }); return; } const port = script.web_ui_port ?? 80; const url = `http://${script.web_ui_ip}:${port}`; - window.open(url, '_blank', 'noopener,noreferrer'); + window.open(url, "_blank", "noopener,noreferrer"); }; // Helper function to check if a script has any actions available const hasActions = (script: InstalledScript) => { - if (script.container_id && script.execution_mode === 'ssh') return true; + if (script.container_id && script.execution_mode === "ssh") return true; if (script.web_ui_ip != null) return true; - if (!script.container_id || script.execution_mode !== 'ssh') return true; + if (!script.container_id || script.execution_mode !== "ssh") return true; return false; }; - const formatDate = (dateString: string) => { return new Date(dateString).toLocaleString(); }; if (isLoading) { return ( -
-
Loading installed scripts...
+
+
+ Loading installed scripts... +
); } @@ -1046,15 +1218,27 @@ export function InstalledScriptsTab() { {updatingScript && (
)} @@ -1065,7 +1249,7 @@ export function InstalledScriptsTab() { -

Installed Scripts

- +
+

+ Installed Scripts +

+ {stats && ( -
-
-
{stats.total}
-
Total Installations
+
+
+
{stats.total}
+
Total Installations
-
-
- {scriptsWithStatus.filter(script => script.container_status === 'running' && !script.is_vm).length} +
+
+ { + scriptsWithStatus.filter( + (script) => + script.container_status === "running" && !script.is_vm, + ).length + }
-
Running LXC
+
Running LXC
-
-
- {scriptsWithStatus.filter(script => script.container_status === 'running' && script.is_vm).length} +
+
+ { + scriptsWithStatus.filter( + (script) => + script.container_status === "running" && script.is_vm, + ).length + }
-
Running VMs
+
Running VMs
-
-
- {scriptsWithStatus.filter(script => script.container_status === 'stopped' && !script.is_vm).length} +
+
+ { + scriptsWithStatus.filter( + (script) => + script.container_status === "stopped" && !script.is_vm, + ).length + }
-
Stopped LXC
+
Stopped LXC
-
-
- {scriptsWithStatus.filter(script => script.container_status === 'stopped' && script.is_vm).length} +
+
+ { + scriptsWithStatus.filter( + (script) => + script.container_status === "stopped" && script.is_vm, + ).length + }
-
Stopped VMs
+
Stopped VMs
)} {/* Add Script and Auto-Detect Buttons */} -
+
{/* Add Script Form */} {showAddForm && ( -
-

Add Manual Script Entry

+
+

+ Add Manual Script Entry +

-
-
-
-
+
@@ -1230,29 +1454,49 @@ export function InstalledScriptsTab() {
{/* Auto-Detect Status Message */} {autoDetectStatus.type && ( -
+
- {autoDetectStatus.type === 'success' ? ( - - + {autoDetectStatus.type === "success" ? ( + + ) : ( - - + + )}
-

+

{autoDetectStatus.message}

@@ -1262,29 +1506,49 @@ export function InstalledScriptsTab() { {/* Cleanup Status Message */} {cleanupStatus.type && ( -
+
- {cleanupStatus.type === 'success' ? ( - - + {cleanupStatus.type === "success" ? ( + + ) : ( - - + + )}
-

+

{cleanupStatus.message}

@@ -1296,26 +1560,40 @@ export function InstalledScriptsTab() { {/* Auto-Detect LXC Containers Form */} {showAutoDetectForm && ( -
-

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

+
+

+ 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
    • +
    • + Find containers with "community-script" in + their tags +
    • Extract the container ID and hostname
    • Add them as installed script entries
    @@ -1323,15 +1601,15 @@ export function InstalledScriptsTab() {
- +
-
-
+
@@ -1373,16 +1653,24 @@ export function InstalledScriptsTab() { placeholder="Search scripts, container IDs, or servers..." value={searchTerm} onChange={(e) => 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" + className="border-border bg-card text-foreground placeholder-muted-foreground focus:ring-ring w-full rounded-md border px-3 py-2 focus:ring-2 focus:outline-none" />
- + {/* Filter Dropdowns - Responsive Grid */} -
+
setServerFilter(e.target.value)} - className="w-full px-3 py-2 border border-border rounded-md bg-card text-foreground focus:outline-none focus:ring-2 focus:ring-ring" + className="border-border bg-card text-foreground focus:ring-ring w-full rounded-md border px-3 py-2 focus:ring-2 focus:outline-none" > - {uniqueServers.map(server => ( - + {uniqueServers.map((server) => ( + ))}
@@ -1406,15 +1696,17 @@ export function InstalledScriptsTab() {
{/* Scripts Display - Mobile Cards / Desktop Table */} -
+
{filteredScripts.length === 0 ? ( -
- {scripts.length === 0 ? 'No installed scripts found.' : 'No scripts match your filters.'} +
+ {scripts.length === 0 + ? "No installed scripts found." + : "No scripts match your filters."}
) : ( <> {/* Mobile Card Layout */} -
+
{filteredScripts.map((script) => ( handleDeleteScript(Number(script.id))} isUpdating={updateScriptMutation.isPending} isDeleting={deleteScriptMutation.isPending} - containerStatus={containerStatuses.get(script.id) ?? 'unknown'} + containerStatus={ + containerStatuses.get(script.id) ?? "unknown" + } onStartStop={(action) => handleStartStop(script, action)} onDestroy={() => handleDestroy(script)} isControlling={controllingScriptId === script.id} @@ -1443,98 +1737,102 @@ export function InstalledScriptsTab() {
{/* Desktop Table Layout */} -
+
- - - - - - - {filteredScripts.map((script) => ( - - - -
handleSort('script_name')} + handleSort("script_name")} >
Script Name - {sortField === 'script_name' && ( + {sortField === "script_name" && ( - {sortDirection === 'asc' ? '↑' : '↓'} + {sortDirection === "asc" ? "↑" : "↓"} )}
handleSort('container_id')} + handleSort("container_id")} >
Container ID - {sortField === 'container_id' && ( + {sortField === "container_id" && ( - {sortDirection === 'asc' ? '↑' : '↓'} + {sortDirection === "asc" ? "↑" : "↓"} )}
+ Web UI handleSort('server_name')} + handleSort("server_name")} >
Server - {sortField === 'server_name' && ( + {sortField === "server_name" && ( - {sortDirection === 'asc' ? '↑' : '↓'} + {sortDirection === "asc" ? "↑" : "↓"} )}
handleSort('status')} + handleSort("status")} >
Status - {sortField === 'status' && ( + {sortField === "status" && ( - {sortDirection === 'asc' ? '↑' : '↓'} + {sortDirection === "asc" ? "↑" : "↓"} )}
handleSort('installation_date')} + handleSort("installation_date")} >
Installation Date - {sortField === 'installation_date' && ( + {sortField === "installation_date" && ( - {sortDirection === 'asc' ? '↑' : '↓'} + {sortDirection === "asc" ? "↑" : "↓"} )}
+ Actions
{editingScriptId === script.id ? ( -
+
handleInputChange('script_name', e.target.value)} - className="w-full px-3 py-2 text-sm font-medium border border-input rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring" + onChange={(e) => + handleInputChange("script_name", e.target.value) + } + className="border-input bg-background text-foreground focus:ring-ring focus:border-ring w-full rounded-md border px-3 py-2 text-sm font-medium focus:ring-2 focus:outline-none" placeholder="Script name" />
@@ -1542,131 +1840,164 @@ export function InstalledScriptsTab() {
{script.container_id && ( - - {script.is_vm ? 'VM' : 'LXC'} + + {script.is_vm ? "VM" : "LXC"} )} -
{script.script_name}
+
+ {script.script_name} +
+
+
+ {script.script_path}
-
{script.script_path}
)}
{editingScriptId === script.id ? ( -
+
handleInputChange('container_id', e.target.value)} - className="w-full px-3 py-2 text-sm font-mono border border-input rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring" + onChange={(e) => + handleInputChange( + "container_id", + e.target.value, + ) + } + className="border-input bg-background text-foreground focus:ring-ring focus:border-ring w-full rounded-md border px-3 py-2 font-mono text-sm focus:ring-2 focus:outline-none" placeholder="Container ID" />
+ ) : script.container_id ? ( +
+ + {String(script.container_id)} + + {script.container_status && ( +
+
+ + {script.container_status === "running" + ? "Running" + : script.container_status === "stopped" + ? "Stopped" + : "Unknown"} + +
+ )} +
) : ( - script.container_id ? ( -
- {String(script.container_id)} - {script.container_status && ( -
-
- - {script.container_status === 'running' ? 'Running' : - script.container_status === 'stopped' ? 'Stopped' : - 'Unknown'} - -
- )} -
- ) : ( - - - ) + + - + )}
{editingScriptId === script.id ? ( -
+
handleInputChange('web_ui_ip', e.target.value)} - className="w-40 px-3 py-2 text-sm font-mono border border-input rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring" + onChange={(e) => + handleInputChange("web_ui_ip", e.target.value) + } + className="border-input bg-background text-foreground focus:ring-ring focus:border-ring w-40 rounded-md border px-3 py-2 font-mono text-sm focus:ring-2 focus:outline-none" placeholder="IP" /> : handleInputChange('web_ui_port', e.target.value)} - className="w-20 px-3 py-2 text-sm font-mono border border-input rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring" + onChange={(e) => + handleInputChange("web_ui_port", e.target.value) + } + className="border-input bg-background text-foreground focus:ring-ring focus:border-ring w-20 rounded-md border px-3 py-2 font-mono text-sm focus:ring-2 focus:outline-none" placeholder="Port" />
+ ) : script.web_ui_ip ? ( +
+ + {script.web_ui_ip}:{script.web_ui_port ?? 80} + + {containerStatuses.get(script.id) === "running" && ( + + )} +
) : ( - script.web_ui_ip ? ( -
- - {script.web_ui_ip}:{script.web_ui_port ?? 80} - - {containerStatuses.get(script.id) === 'running' && ( - - )} -
- ) : ( -
- - - {script.container_id && script.execution_mode === 'ssh' && ( +
+ + - + + {script.container_id && + script.execution_mode === "ssh" && ( )} -
- ) +
)}
- + - {script.server_name ?? '-'} + {script.server_name ?? "-"} - {script.status.replace('_', ' ').toUpperCase()} + {script.status.replace("_", " ").toUpperCase()} + {formatDate(String(script.installation_date))} +
{editingScriptId === script.id ? ( <> @@ -1676,7 +2007,9 @@ export function InstalledScriptsTab() { variant="save" size="sm" > - {updateScriptMutation.isPending ? 'Saving...' : 'Save'} + {updateScriptMutation.isPending + ? "Saving..." + : "Save"} - + {script.container_id && !script.is_vm && ( handleUpdateScript(script)} - disabled={containerStatuses.get(script.id) === 'stopped'} + onClick={() => + handleUpdateScript(script) + } + disabled={ + containerStatuses.get(script.id) === + "stopped" + } className="text-muted-foreground hover:text-foreground hover:bg-muted/20 focus:bg-muted/20" > Update )} - {script.container_id && script.execution_mode === 'ssh' && ( - handleBackupScript(script)} - disabled={containerStatuses.get(script.id) === 'stopped'} - className="text-muted-foreground hover:text-foreground hover:bg-muted/20 focus:bg-muted/20" - > - Backup - - )} - {script.container_id && script.execution_mode === 'ssh' && !script.is_vm && ( - handleOpenShell(script)} - disabled={containerStatuses.get(script.id) === 'stopped'} - className="text-muted-foreground hover:text-foreground hover:bg-muted/20 focus:bg-muted/20" - > - Shell - - )} + {script.container_id && + script.execution_mode === "ssh" && ( + + handleBackupScript(script) + } + disabled={ + containerStatuses.get(script.id) === + "stopped" + } + className="text-muted-foreground hover:text-foreground hover:bg-muted/20 focus:bg-muted/20" + > + Backup + + )} + {script.container_id && + script.execution_mode === "ssh" && + !script.is_vm && ( + + handleOpenShell(script) + } + disabled={ + containerStatuses.get(script.id) === + "stopped" + } + className="text-muted-foreground hover:text-foreground hover:bg-muted/20 focus:bg-muted/20" + > + Shell + + )} {script.web_ui_ip && ( handleOpenWebUI(script)} - disabled={containerStatuses.get(script.id) === 'stopped'} + disabled={ + containerStatuses.get(script.id) === + "stopped" + } className="text-muted-foreground hover:text-foreground hover:bg-muted/20 focus:bg-muted/20" > Open UI )} - {script.container_id && script.execution_mode === 'ssh' && script.web_ui_ip && ( - handleAutoDetectWebUI(script)} - disabled={autoDetectWebUIMutation.isPending ?? containerStatuses.get(script.id) === 'stopped'} - className="text-muted-foreground hover:text-foreground hover:bg-muted/20 focus:bg-muted/20" - > - {autoDetectWebUIMutation.isPending ? 'Re-detect...' : 'Re-detect IP/Port'} - - )} - {script.container_id && script.execution_mode === 'ssh' && !script.is_vm && ( - <> - + {script.container_id && + script.execution_mode === "ssh" && + script.web_ui_ip && ( handleLXCSettings(script)} - className="text-muted-foreground hover:text-foreground hover:bg-muted/20 focus:bg-muted/20" - > - - LXC Settings - - - - )} - {script.container_id && script.execution_mode === 'ssh' && ( - <> - {script.is_vm && } - handleStartStop(script, (containerStatuses.get(script.id) ?? 'unknown') === 'running' ? 'stop' : 'start')} - disabled={controllingScriptId === script.id || (containerStatuses.get(script.id) ?? 'unknown') === 'unknown'} - className={(containerStatuses.get(script.id) ?? 'unknown') === 'running' - ? "text-error hover:text-error-foreground hover:bg-error/20 focus:bg-error/20" - : "text-success hover:text-success-foreground hover:bg-success/20 focus:bg-success/20" + onClick={() => + handleAutoDetectWebUI(script) + } + disabled={ + autoDetectWebUIMutation.isPending ?? + containerStatuses.get(script.id) === + "stopped" } - > - {controllingScriptId === script.id ? 'Working...' : (containerStatuses.get(script.id) ?? 'unknown') === 'running' ? 'Stop' : 'Start'} - - handleDestroy(script)} - disabled={controllingScriptId === script.id} - className="text-error hover:text-error-foreground hover:bg-error/20 focus:bg-error/20" - > - {controllingScriptId === script.id ? 'Working...' : 'Destroy'} - - - handleDeleteScript(script.id, script)} - disabled={deleteScriptMutation.isPending} className="text-muted-foreground hover:text-foreground hover:bg-muted/20 focus:bg-muted/20" > - {deleteScriptMutation.isPending ? 'Deleting...' : 'Delete only from DB'} + {autoDetectWebUIMutation.isPending + ? "Re-detect..." + : "Re-detect IP/Port"} - - )} - {(!script.container_id || script.execution_mode !== 'ssh') && ( + )} + {script.container_id && + script.execution_mode === "ssh" && + !script.is_vm && ( + <> + + + handleLXCSettings(script) + } + className="text-muted-foreground hover:text-foreground hover:bg-muted/20 focus:bg-muted/20" + > + + LXC Settings + + + + )} + {script.container_id && + script.execution_mode === "ssh" && ( + <> + {script.is_vm && ( + + )} + + handleStartStop( + script, + (containerStatuses.get( + script.id, + ) ?? "unknown") === "running" + ? "stop" + : "start", + ) + } + disabled={ + controllingScriptId === + script.id || + (containerStatuses.get( + script.id, + ) ?? "unknown") === "unknown" + } + className={ + (containerStatuses.get( + script.id, + ) ?? "unknown") === "running" + ? "text-error hover:text-error-foreground hover:bg-error/20 focus:bg-error/20" + : "text-success hover:text-success-foreground hover:bg-success/20 focus:bg-success/20" + } + > + {controllingScriptId === script.id + ? "Working..." + : (containerStatuses.get( + script.id, + ) ?? "unknown") === "running" + ? "Stop" + : "Start"} + + + handleDestroy(script) + } + disabled={ + controllingScriptId === script.id + } + className="text-error hover:text-error-foreground hover:bg-error/20 focus:bg-error/20" + > + {controllingScriptId === script.id + ? "Working..." + : "Destroy"} + + + + handleDeleteScript( + script.id, + script, + ) + } + disabled={ + deleteScriptMutation.isPending + } + className="text-muted-foreground hover:text-foreground hover:bg-muted/20 focus:bg-muted/20" + > + {deleteScriptMutation.isPending + ? "Deleting..." + : "Delete only from DB"} + + + )} + {(!script.container_id || + script.execution_mode !== "ssh") && ( <> handleDeleteScript(Number(script.id))} - disabled={deleteScriptMutation.isPending} + onClick={() => + handleDeleteScript( + Number(script.id), + ) + } + disabled={ + deleteScriptMutation.isPending + } className="text-muted-foreground hover:text-foreground hover:bg-muted/20 focus:bg-muted/20" > - {deleteScriptMutation.isPending ? 'Deleting...' : 'Delete'} + {deleteScriptMutation.isPending + ? "Deleting..." + : "Delete"} )} @@ -1823,122 +2242,134 @@ export function InstalledScriptsTab() { )}
- {/* Confirmation Modal */} - {confirmationModal && ( - setConfirmationModal(null)} - onConfirm={confirmationModal.onConfirm} - title={confirmationModal.title} - message={confirmationModal.message} - variant={confirmationModal.variant} - confirmText={confirmationModal.confirmText} - /> - )} + {/* Confirmation Modal */} + {confirmationModal && ( + setConfirmationModal(null)} + onConfirm={confirmationModal.onConfirm} + title={confirmationModal.title} + message={confirmationModal.message} + variant={confirmationModal.variant} + confirmText={confirmationModal.confirmText} + /> + )} - {/* Error/Success Modal */} - {errorModal && ( - setErrorModal(null)} - title={errorModal.title} - message={errorModal.message} - details={errorModal.details} - type={errorModal.type ?? 'error'} - /> - )} + {/* Error/Success Modal */} + {errorModal && ( + setErrorModal(null)} + title={errorModal.title} + message={errorModal.message} + details={errorModal.details} + type={errorModal.type ?? "error"} + /> + )} - {/* Loading Modal */} - {loadingModal && ( - - )} + {/* Loading Modal */} + {loadingModal && ( + + )} - {/* Backup Prompt Modal */} - {showBackupPrompt && ( -
-
-
-
- - - -

Backup Before Update?

-
+ {/* Backup Prompt Modal */} + {showBackupPrompt && ( +
+
+
+
+ + + +

+ Backup Before Update? +

-
-

- Would you like to create a backup before updating the container? -

-
- - -
+
+
+

+ Would you like to create a backup before updating the container? +

+
+ +
- )} +
+ )} - {/* Storage Selection Modal */} - { - setShowStorageSelection(false); - setPendingUpdateScript(null); - setBackupStorages([]); - }} - onSelect={handleStorageSelected} - storages={backupStorages} - isLoading={isLoadingStorages} - onRefresh={() => { - if (pendingUpdateScript?.server_id) { - void fetchStorages(pendingUpdateScript.server_id, true); - } - }} - /> + {/* Storage Selection Modal */} + { + setShowStorageSelection(false); + setPendingUpdateScript(null); + setBackupStorages([]); + }} + onSelect={handleStorageSelected} + storages={backupStorages} + isLoading={isLoadingStorages} + onRefresh={() => { + if (pendingUpdateScript?.server_id) { + void fetchStorages(pendingUpdateScript.server_id, true); + } + }} + /> - {/* Backup Warning Modal */} - setShowBackupWarning(false)} - onProceed={() => { - setShowBackupWarning(false); - // Proceed with update even though backup failed - if (pendingUpdateScript) { - proceedWithUpdate(null); - } - }} - /> + {/* Backup Warning Modal */} + setShowBackupWarning(false)} + onProceed={() => { + setShowBackupWarning(false); + // Proceed with update even though backup failed + if (pendingUpdateScript) { + proceedWithUpdate(null); + } + }} + /> - {/* LXC Settings Modal */} - setLxcSettingsModal({ isOpen: false, script: null })} - onSave={() => { - setLxcSettingsModal({ isOpen: false, script: null }); - void refetchScripts(); - }} - /> + {/* LXC Settings Modal */} + setLxcSettingsModal({ isOpen: false, script: null })} + onSave={() => { + setLxcSettingsModal({ isOpen: false, script: null }); + void refetchScripts(); + }} + />
); } diff --git a/src/server/services/githubJsonService.ts b/src/server/services/githubJsonService.ts index 650b59c..32a4f6e 100644 --- a/src/server/services/githubJsonService.ts +++ b/src/server/services/githubJsonService.ts @@ -398,7 +398,6 @@ export class GitHubJsonService { const filesToSync: GitHubFile[] = []; for (const ghFile of githubFiles) { - const slug = ghFile.name.replace('.json', ''); const localFilePath = join(this.localJsonDirectory!, ghFile.name); let needsSync = false;