From c266c4cb3c8bf709a4603a37a00de4de8f1ce816 Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Fri, 28 Nov 2025 12:16:18 +0100 Subject: [PATCH] Refactor InstalledScriptsTab for code style consistency Updated InstalledScriptsTab.tsx to use double quotes and consistent formatting throughout the file. Improved type annotations, code readability, and standardized state initialization and mutation usage. No functional changes were made; this is a style and maintainability refactor. --- src/app/_components/InstalledScriptsTab.tsx | 2103 +++++++++++-------- src/server/services/githubJsonService.ts | 1 - 2 files changed, 1267 insertions(+), 837 deletions(-) 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;