feat: Add LXC Container Control Features (#124)
* feat: Add LXC container control functionality to Installed Scripts page - Add reusable ConfirmationModal component with simple and type-to-confirm variants - Add three new tRPC endpoints for container control: - getContainerStatus: Check container running/stopped state via pct status - controlContainer: Start/stop containers via pct start/stop commands - destroyContainer: Destroy containers via pct destroy and delete DB records - Enhance InstalledScriptsTab with container status management and confirmation flows - Update ScriptInstallationCard with Start/Stop and Destroy buttons for SSH scripts - Add container control buttons to desktop table view with proper status handling - Update help documentation with comprehensive container control feature guide - Implement safety features: - Simple OK/Cancel confirmation for start/stop actions - Type-to-confirm modal requiring container ID for destroy actions - SSH connection validation and error handling - Loading states and user feedback for all operations - Only show control buttons for SSH scripts with valid container IDs - Maintain backward compatibility with existing delete functionality for non-SSH scripts All container control operations execute via SSH using existing infrastructure. Real-time container status checking and caching for optimal performance. * fix: Resolve linting errors in LXC control functionality - Remove unused getStatusMutation variable - Fix floating promises by adding void operator - Add missing dependencies to useEffect hooks - Fix unsafe argument types by casting server IDs to Number - Remove unused commandOutput variables - Use useCallback for fetchContainerStatus to fix dependency issues - Move function definition before usage to resolve hoisting errors * fix: Add missing execution_mode property to InstalledScript interface in ScriptInstallationCard - Add execution_mode: local | ssh property to InstalledScript interface - Fixes TypeScript build error when checking script.execution_mode === ssh - Ensures type consistency across all components * fix: Resolve status detection conflicts by using unified bulk fetching - Remove individual fetchContainerStatus function that was conflicting with bulk fetching - Update controlContainerMutation to use fetchContainerStatuses instead of individual calls - Remove unused utils variable to clean up linting warnings - Simplify status detection to use only the bulk getContainerStatuses endpoint - This should resolve the status detection issues by eliminating competing fetch mechanisms * fix: Stop infinite API call loops that were overwhelming the server - Remove fetchContainerStatuses from useEffect dependencies to prevent infinite loops - Use useRef to access current scripts without causing dependency cycles - Reduce multiple useEffect hooks that were all triggering status checks - This should stop the 30+ simultaneous API calls that were redlining the server - Status checks now happen only when needed: on load, after operations, and every 60s * feat: Implement efficient pct list approach for container status checking - Replace individual container status checks with bulk pct list per server - Update getContainerStatuses to run pct list once per server and parse all results - Simplify frontend to just pass server IDs instead of individual container data - Much more efficient: 1 SSH call per server instead of 1 call per container - Parse pct list output format: CTID Status Name - Map pct list status (running/stopped) to our status format - This should resolve the server overload issues while maintaining functionality * fix: Remove duplicate container status display from STATUS column - Remove container runtime status from STATUS column in both desktop and mobile views - Keep container status display next to container ID where it belongs - STATUS column now only shows installation status (SUCCESS/FAILED) - Container runtime status (running/stopped) remains next to container ID - Cleaner UI with no duplicate status information * feat: Trigger status check when switching to installed scripts tab - Add useEffect hook that triggers fetchContainerStatuses when component mounts - This ensures container statuses are refreshed every time user switches to the tab - Improves user experience by always showing current container states - Uses empty dependency array to run only once per tab switch * cleanup: Remove all console.log statements from codebase - Remove console.log statements from InstalledScriptsTab.tsx - Remove console.log statements from installedScripts.ts router - Remove console.log statements from VersionDisplay.tsx - Remove console.log statements from ScriptsGrid.tsx - Keep console.error statements for proper error logging - Cleaner production logs without debug output * feat: Display detailed SSH error messages for container operations - Capture both stdout and stderr from pct start/stop/destroy commands - Show actual SSH error output to users instead of generic error messages - Update controlContainer and destroyContainer to return detailed error messages - Improve frontend error handling to display backend error messages - Users now see specific error details like permission denied, container not found, etc. - Better debugging experience with meaningful error feedback * feat: Auto-stop containers before destroy and improve error UI - Automatically stop running containers before destroying them - Create custom ErrorModal component to replace ugly browser alerts - Support both error and success modal types with appropriate styling - Show detailed SSH error messages in a beautiful modal interface - Update destroy success message to indicate if container was stopped first - Better UX with consistent design language and proper error handling - Auto-close modals after 10 seconds for better user experience * fix: Replace dialog component with custom modal implementation - Remove dependency on non-existent dialog component - Use same modal pattern as ConfirmationModal for consistency - Custom modal with backdrop, proper styling, and responsive design - Maintains all functionality while fixing module resolution error - Consistent with existing codebase patterns * feat: Add instant success feedback for container start/stop operations - Show success modal immediately after start/stop operations - Update container status in UI instantly before background status check - Prevents user confusion by showing expected status change immediately - Add containerId to backend response for proper script identification - Success modals show appropriate messages for start vs stop operations - Background status check still runs to ensure accuracy - Better UX with instant visual feedback * fix: Improve Container Control section styling in help modal - Replace bright red styling with subtle accent colors - Use consistent design language that matches the rest of the interface - Change safety features from red to yellow warning styling - Better visual hierarchy and readability - Maintains warning importance while being less jarring * fix: Make safety features section much more subtle in help modal - Replace bright yellow with muted background colors - Use standard text colors (text-foreground, text-muted-foreground) - Maintains warning icon but with consistent styling - Much less jarring against dark theme - Better integration with overall design language * feat: Replace update script alerts with custom confirmation modal - Replace browser alert() with custom ErrorModal for validation errors - Replace browser confirm() with custom ConfirmationModal for update confirmation - Add type-to-confirm safety feature requiring container ID input - Include data loss warning and backup recommendation in confirmation message - Consistent UI/UX with other confirmation dialogs - Better error messaging with detailed information * fix: Resolve all build errors and warnings - Fix nullish coalescing operator warnings (|| to ??) - Remove unused imports and variables - Fix TypeScript type errors with proper casting - Update ConfirmationModal state type to include missing properties - Fix useEffect dependency warnings - All build errors resolved, only minor unused variable warning remains - Build now passes successfully * feat: Disable update button when container is stopped - Add disabled condition to update button in table view - Add disabled condition to update button in mobile card view - Prevents users from updating stopped containers - Uses containerStatus to determine if button should be disabled - Improves UX by preventing invalid operations on stopped containers * fix: Resolve infinite loop in status updates - Remove containerStatusMutation from fetchContainerStatuses dependencies - Use empty dependency array for fetchContainerStatuses useCallback - Remove fetchContainerStatuses from useEffect dependencies - Only depend on scripts.length to prevent infinite loops - Status updates now run only when scripts change, not on every render * fix: Correct misleading text in update confirmation modal - Change "will re-run the script installation process" to "will update the script" - More accurate description of what the update operation actually does - Maintains warning about potential container impact and backup recommendation - Better user understanding of the actual operation being performed * refactor: Remove all comments from InstalledScriptsTab.tsx - Remove all single-line comments (//) - Remove all multi-line comments (/* */) - Clean up excessive empty lines - Improve code readability and reduce file size - Maintain all functionality while removing documentation comments * refactor: Improve code organization and add comprehensive comments - Add clear section comments for better code organization - Document all major state variables and their purposes - Add detailed comments for complex logic and operations - Improve readability with better spacing and structure - Maintain all existing functionality while improving maintainability - Add comments for container control, mutations, and UI sections
This commit is contained in:
committed by
GitHub
parent
53b5074f35
commit
5b45293b4d
111
src/app/_components/ConfirmationModal.tsx
Normal file
111
src/app/_components/ConfirmationModal.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button } from './ui/button';
|
||||
import { AlertTriangle, Info } from 'lucide-react';
|
||||
|
||||
interface ConfirmationModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
title: string;
|
||||
message: string;
|
||||
variant: 'simple' | 'danger';
|
||||
confirmText?: string; // What the user must type for danger variant
|
||||
confirmButtonText?: string;
|
||||
cancelButtonText?: string;
|
||||
}
|
||||
|
||||
export function ConfirmationModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm,
|
||||
title,
|
||||
message,
|
||||
variant,
|
||||
confirmText,
|
||||
confirmButtonText = 'Confirm',
|
||||
cancelButtonText = 'Cancel'
|
||||
}: ConfirmationModalProps) {
|
||||
const [typedText, setTypedText] = useState('');
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const isDanger = variant === 'danger';
|
||||
const isConfirmEnabled = isDanger ? typedText === confirmText : true;
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (isConfirmEnabled) {
|
||||
onConfirm();
|
||||
setTypedText(''); // Reset for next time
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
setTypedText(''); // Reset when closing
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-card rounded-lg shadow-xl max-w-md w-full border border-border">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-center p-6 border-b border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
{isDanger ? (
|
||||
<AlertTriangle className="h-8 w-8 text-red-600" />
|
||||
) : (
|
||||
<Info className="h-8 w-8 text-blue-600" />
|
||||
)}
|
||||
<h2 className="text-2xl font-bold text-card-foreground">{title}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6">
|
||||
<p className="text-sm text-muted-foreground mb-6">
|
||||
{message}
|
||||
</p>
|
||||
|
||||
{/* Type-to-confirm input for danger variant */}
|
||||
{isDanger && confirmText && (
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Type <code className="bg-muted px-2 py-1 rounded text-sm">{confirmText}</code> to confirm:
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={typedText}
|
||||
onChange={(e) => setTypedText(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-input rounded-md bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring"
|
||||
placeholder={`Type "${confirmText}" here`}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-col sm:flex-row justify-end gap-3">
|
||||
<Button
|
||||
onClick={handleClose}
|
||||
variant="outline"
|
||||
size="default"
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{cancelButtonText}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleConfirm}
|
||||
disabled={!isConfirmEnabled}
|
||||
variant={isDanger ? "destructive" : "default"}
|
||||
size="default"
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{confirmButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
87
src/app/_components/ErrorModal.tsx
Normal file
87
src/app/_components/ErrorModal.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { Button } from './ui/button';
|
||||
import { AlertCircle, CheckCircle } from 'lucide-react';
|
||||
|
||||
interface ErrorModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
message: string;
|
||||
details?: string;
|
||||
type?: 'error' | 'success';
|
||||
}
|
||||
|
||||
export function ErrorModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
title,
|
||||
message,
|
||||
details,
|
||||
type = 'error'
|
||||
}: ErrorModalProps) {
|
||||
// Auto-close after 10 seconds
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const timer = setTimeout(() => {
|
||||
onClose();
|
||||
}, 10000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-card rounded-lg shadow-xl max-w-lg w-full border border-border">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-center p-6 border-b border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
{type === 'success' ? (
|
||||
<CheckCircle className="h-8 w-8 text-green-600 dark:text-green-400" />
|
||||
) : (
|
||||
<AlertCircle className="h-8 w-8 text-red-600 dark:text-red-400" />
|
||||
)}
|
||||
<h2 className="text-xl font-semibold text-foreground">{title}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6">
|
||||
<p className="text-sm text-foreground mb-4">{message}</p>
|
||||
{details && (
|
||||
<div className={`rounded-lg p-3 ${
|
||||
type === 'success'
|
||||
? 'bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-800'
|
||||
: 'bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-800'
|
||||
}`}>
|
||||
<p className={`text-xs font-medium mb-1 ${
|
||||
type === 'success'
|
||||
? 'text-green-800 dark:text-green-200'
|
||||
: 'text-red-800 dark:text-red-200'
|
||||
}`}>
|
||||
{type === 'success' ? 'Details:' : 'Error Details:'}
|
||||
</p>
|
||||
<pre className={`text-xs whitespace-pre-wrap break-words ${
|
||||
type === 'success'
|
||||
? 'text-green-700 dark:text-green-300'
|
||||
: 'text-red-700 dark:text-red-300'
|
||||
}`}>
|
||||
{details}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-end gap-3 p-6 border-t border-border">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -334,6 +334,29 @@ export function HelpModal({ isOpen, onClose, initialSection = 'server-settings'
|
||||
<li>• <strong>Update Scripts:</strong> Re-run or update existing script installations</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border border-border rounded-lg bg-accent/50 dark:bg-accent/20">
|
||||
<h4 className="font-medium text-foreground mb-2">Container Control (NEW)</h4>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
Directly control LXC containers from the installed scripts page via SSH.
|
||||
</p>
|
||||
<ul className="text-sm text-muted-foreground space-y-2">
|
||||
<li>• <strong>Start/Stop Button:</strong> Control container state with <code>pct start/stop <ID></code></li>
|
||||
<li>• <strong>Container Status:</strong> Real-time status indicator (running/stopped/unknown)</li>
|
||||
<li>• <strong>Destroy Button:</strong> Permanently remove LXC container with <code>pct destroy <ID></code></li>
|
||||
<li>• <strong>Confirmation Modals:</strong> Simple OK/Cancel for start/stop, type container ID to confirm destroy</li>
|
||||
<li>• <strong>SSH Execution:</strong> All commands executed remotely via configured SSH connections</li>
|
||||
</ul>
|
||||
<div className="mt-3 p-3 bg-muted/30 dark:bg-muted/20 rounded-lg border border-border">
|
||||
<p className="text-sm font-medium text-foreground">⚠️ Safety Features:</p>
|
||||
<ul className="text-sm text-muted-foreground mt-1 space-y-1">
|
||||
<li>• Start/Stop actions require simple confirmation</li>
|
||||
<li>• Destroy action requires typing the container ID to confirm</li>
|
||||
<li>• All actions show loading states and error handling</li>
|
||||
<li>• Only works with SSH scripts that have valid container IDs</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,8 @@ 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 { getContrastColor } from '../../lib/colorUtils';
|
||||
|
||||
interface InstalledScript {
|
||||
@@ -22,6 +24,7 @@ interface InstalledScript {
|
||||
installation_date: string;
|
||||
status: 'in_progress' | 'success' | 'failed';
|
||||
output_log: string | null;
|
||||
execution_mode: 'local' | 'ssh';
|
||||
container_status?: 'running' | 'stopped' | 'unknown';
|
||||
}
|
||||
|
||||
@@ -41,7 +44,30 @@ export function InstalledScriptsTab() {
|
||||
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);
|
||||
const [containerStatuses, setContainerStatuses] = useState<Record<string, 'running' | 'stopped' | 'unknown'>>({});
|
||||
|
||||
// Container control state
|
||||
const [containerStatuses, setContainerStatuses] = useState<Map<number, 'running' | 'stopped' | 'unknown'>>(new Map());
|
||||
const [confirmationModal, setConfirmationModal] = useState<{
|
||||
isOpen: boolean;
|
||||
variant: 'simple' | 'danger';
|
||||
title: string;
|
||||
message: string;
|
||||
confirmText?: string;
|
||||
confirmButtonText?: string;
|
||||
cancelButtonText?: string;
|
||||
onConfirm: () => void;
|
||||
} | null>(null);
|
||||
const [controllingScriptId, setControllingScriptId] = useState<number | null>(null);
|
||||
const scriptsRef = useRef<InstalledScript[]>([]);
|
||||
|
||||
// Error modal state
|
||||
const [errorModal, setErrorModal] = useState<{
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
details?: string;
|
||||
type?: 'error' | 'success';
|
||||
} | null>(null);
|
||||
|
||||
// Fetch installed scripts
|
||||
const { data: scriptsData, refetch: refetchScripts, isLoading } = api.installedScripts.getAllInstalledScripts.useQuery();
|
||||
@@ -82,7 +108,6 @@ export function InstalledScriptsTab() {
|
||||
// Auto-detect LXC containers mutation
|
||||
const autoDetectMutation = api.installedScripts.autoDetectLXCContainers.useMutation({
|
||||
onSuccess: (data) => {
|
||||
console.log('Auto-detect success:', data);
|
||||
void refetchScripts();
|
||||
setShowAutoDetectForm(false);
|
||||
setAutoDetectServerId('');
|
||||
@@ -120,7 +145,28 @@ export function InstalledScriptsTab() {
|
||||
const containerStatusMutation = api.installedScripts.getContainerStatuses.useMutation({
|
||||
onSuccess: (data) => {
|
||||
if (data.success) {
|
||||
setContainerStatuses(data.statusMap);
|
||||
|
||||
// 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');
|
||||
}
|
||||
});
|
||||
|
||||
setContainerStatuses(statusMap);
|
||||
} else {
|
||||
console.error('Container status fetch failed:', data.error);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
@@ -131,7 +177,6 @@ export function InstalledScriptsTab() {
|
||||
// Cleanup orphaned scripts mutation
|
||||
const cleanupMutation = api.installedScripts.cleanupOrphanedScripts.useMutation({
|
||||
onSuccess: (data) => {
|
||||
console.log('Cleanup success:', data);
|
||||
void refetchScripts();
|
||||
|
||||
if (data.deletedCount > 0) {
|
||||
@@ -159,76 +204,149 @@ export function InstalledScriptsTab() {
|
||||
}
|
||||
});
|
||||
|
||||
// Container control mutations
|
||||
// Note: getStatusMutation removed - using direct API calls instead
|
||||
|
||||
const controlContainerMutation = api.installedScripts.controlContainer.useMutation({
|
||||
onSuccess: (data, variables) => {
|
||||
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;
|
||||
});
|
||||
|
||||
// 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)
|
||||
fetchContainerStatuses();
|
||||
} 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);
|
||||
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) => {
|
||||
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);
|
||||
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 stats = statsData?.stats;
|
||||
|
||||
// Function to fetch container statuses
|
||||
const fetchContainerStatuses = useCallback(() => {
|
||||
console.log('Fetching container statuses...', { scriptsCount: scripts.length });
|
||||
const containersWithIds = scripts
|
||||
.filter(script => script.container_id)
|
||||
.map(script => ({
|
||||
containerId: script.container_id!,
|
||||
serverId: script.server_id ?? undefined,
|
||||
server: script.server_id ? {
|
||||
id: script.server_id,
|
||||
name: script.server_name!,
|
||||
ip: script.server_ip!,
|
||||
user: script.server_user!,
|
||||
password: script.server_password!,
|
||||
auth_type: 'password' // Default to password auth
|
||||
} : undefined
|
||||
}));
|
||||
// Update ref when scripts change
|
||||
useEffect(() => {
|
||||
scriptsRef.current = scripts;
|
||||
}, [scripts]);
|
||||
|
||||
console.log('Containers to check:', containersWithIds.length);
|
||||
if (containersWithIds.length > 0) {
|
||||
containerStatusMutation.mutate({ containers: containersWithIds });
|
||||
// Function to fetch container statuses - simplified to just check all servers
|
||||
const fetchContainerStatuses = useCallback(() => {
|
||||
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!))];
|
||||
|
||||
if (serverIds.length > 0) {
|
||||
containerStatusMutation.mutate({ serverIds });
|
||||
}
|
||||
}, [scripts, containerStatusMutation]);
|
||||
}, []); // Empty dependency array to prevent infinite loops
|
||||
|
||||
// Run cleanup when component mounts and scripts are loaded (only once)
|
||||
useEffect(() => {
|
||||
if (scripts.length > 0 && serversData?.servers && !cleanupMutation.isPending && !cleanupRunRef.current) {
|
||||
console.log('Running automatic cleanup check...');
|
||||
cleanupRunRef.current = true;
|
||||
void cleanupMutation.mutate();
|
||||
}
|
||||
}, [scripts.length, serversData?.servers, cleanupMutation]);
|
||||
|
||||
// Auto-refresh container statuses every 60 seconds
|
||||
|
||||
|
||||
// Note: Individual status fetching removed - using bulk fetchContainerStatuses instead
|
||||
|
||||
// Trigger status check when tab becomes active (component mounts)
|
||||
useEffect(() => {
|
||||
if (scripts.length > 0) {
|
||||
fetchContainerStatuses(); // Initial fetch
|
||||
const interval = setInterval(fetchContainerStatuses, 60000); // Every 60 seconds
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [scripts.length, fetchContainerStatuses]);
|
||||
|
||||
// Trigger status check when component becomes visible (tab is active)
|
||||
useEffect(() => {
|
||||
if (scripts.length > 0) {
|
||||
// Small delay to ensure component is fully rendered
|
||||
const timeoutId = setTimeout(() => {
|
||||
fetchContainerStatuses();
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}
|
||||
}, [scripts.length, fetchContainerStatuses]); // Include dependencies
|
||||
|
||||
// Also trigger status check when scripts data loads
|
||||
useEffect(() => {
|
||||
if (scripts.length > 0 && !isLoading) {
|
||||
console.log('Scripts data loaded, triggering status check');
|
||||
fetchContainerStatuses();
|
||||
}
|
||||
}, [scriptsData, isLoading, scripts.length, fetchContainerStatuses]);
|
||||
}, [scripts.length]); // Only depend on scripts.length to prevent infinite loops
|
||||
|
||||
// Update scripts with container statuses
|
||||
const scriptsWithStatus = scripts.map(script => ({
|
||||
...script,
|
||||
container_status: script.container_id ? containerStatuses[script.container_id] ?? 'unknown' : undefined
|
||||
container_status: script.container_id ? containerStatuses.get(script.id) ?? 'unknown' : undefined
|
||||
}));
|
||||
|
||||
// Filter and sort scripts
|
||||
@@ -323,31 +441,86 @@ export function InstalledScriptsTab() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateScript = (script: InstalledScript) => {
|
||||
// Container control handlers
|
||||
const handleStartStop = (script: InstalledScript, action: 'start' | 'stop') => {
|
||||
if (!script.container_id) {
|
||||
alert('No Container ID available for this script');
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirm(`Are you sure you want to update ${script.script_name}?`)) {
|
||||
// Get server info if it's SSH mode
|
||||
let server = null;
|
||||
if (script.server_id && script.server_user && script.server_password) {
|
||||
server = {
|
||||
id: script.server_id,
|
||||
name: script.server_name,
|
||||
ip: script.server_ip,
|
||||
user: script.server_user,
|
||||
password: script.server_password
|
||||
};
|
||||
|
||||
setConfirmationModal({
|
||||
isOpen: true,
|
||||
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);
|
||||
void controlContainerMutation.mutate({ id: script.id, action });
|
||||
setConfirmationModal(null);
|
||||
}
|
||||
|
||||
setUpdatingScript({
|
||||
id: script.id,
|
||||
containerId: script.container_id,
|
||||
server: server
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleDestroy = (script: InstalledScript) => {
|
||||
if (!script.container_id) {
|
||||
alert('No Container ID available for this script');
|
||||
return;
|
||||
}
|
||||
|
||||
setConfirmationModal({
|
||||
isOpen: true,
|
||||
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);
|
||||
void destroyContainerMutation.mutate({ id: script.id });
|
||||
setConfirmationModal(null);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdateScript = (script: InstalledScript) => {
|
||||
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.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Show confirmation modal with type-to-confirm for update
|
||||
setConfirmationModal({
|
||||
isOpen: true,
|
||||
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',
|
||||
confirmText: script.container_id,
|
||||
confirmButtonText: 'Update Script',
|
||||
onConfirm: () => {
|
||||
// Get server info if it's SSH mode
|
||||
let server = null;
|
||||
if (script.server_id && script.server_user && script.server_password) {
|
||||
server = {
|
||||
id: script.server_id,
|
||||
name: script.server_name,
|
||||
ip: script.server_ip,
|
||||
user: script.server_user,
|
||||
password: script.server_password
|
||||
};
|
||||
}
|
||||
|
||||
setUpdatingScript({
|
||||
id: script.id,
|
||||
containerId: script.container_id!,
|
||||
server: server
|
||||
});
|
||||
setConfirmationModal(null);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleCloseUpdateTerminal = () => {
|
||||
@@ -369,7 +542,12 @@ export function InstalledScriptsTab() {
|
||||
|
||||
const handleSaveEdit = () => {
|
||||
if (!editFormData.script_name.trim()) {
|
||||
alert('Script name is required');
|
||||
setErrorModal({
|
||||
isOpen: true,
|
||||
title: 'Validation Error',
|
||||
message: 'Script name is required',
|
||||
details: 'Please enter a valid script name before saving.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -427,7 +605,6 @@ export function InstalledScriptsTab() {
|
||||
}
|
||||
|
||||
setAutoDetectStatus({ type: null, message: '' });
|
||||
console.log('Starting auto-detect for server ID:', autoDetectServerId);
|
||||
autoDetectMutation.mutate({ serverId: Number(autoDetectServerId) });
|
||||
};
|
||||
|
||||
@@ -798,6 +975,10 @@ export function InstalledScriptsTab() {
|
||||
onDelete={() => handleDeleteScript(Number(script.id))}
|
||||
isUpdating={updateScriptMutation.isPending}
|
||||
isDeleting={deleteScriptMutation.isPending}
|
||||
containerStatus={containerStatuses.get(script.id) ?? 'unknown'}
|
||||
onStartStop={(action) => handleStartStop(script, action)}
|
||||
onDestroy={() => handleDestroy(script)}
|
||||
isControlling={controllingScriptId === script.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -993,18 +1174,43 @@ export function InstalledScriptsTab() {
|
||||
onClick={() => handleUpdateScript(script)}
|
||||
variant="update"
|
||||
size="sm"
|
||||
disabled={containerStatuses.get(script.id) === 'stopped'}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => handleDeleteScript(Number(script.id))}
|
||||
variant="delete"
|
||||
size="sm"
|
||||
disabled={deleteScriptMutation.isPending}
|
||||
>
|
||||
{deleteScriptMutation.isPending ? 'Deleting...' : 'Delete'}
|
||||
</Button>
|
||||
{/* Container Control Buttons - only show for SSH scripts with container_id */}
|
||||
{script.container_id && script.execution_mode === 'ssh' && (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => handleStartStop(script, (containerStatuses.get(script.id) ?? 'unknown') === 'running' ? 'stop' : 'start')}
|
||||
disabled={controllingScriptId === script.id || (containerStatuses.get(script.id) ?? 'unknown') === 'unknown'}
|
||||
variant={(containerStatuses.get(script.id) ?? 'unknown') === 'running' ? 'destructive' : 'default'}
|
||||
size="sm"
|
||||
>
|
||||
{controllingScriptId === script.id ? 'Working...' : (containerStatuses.get(script.id) ?? 'unknown') === 'running' ? 'Stop' : 'Start'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleDestroy(script)}
|
||||
disabled={controllingScriptId === script.id}
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
>
|
||||
{controllingScriptId === script.id ? 'Working...' : 'Destroy'}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{/* Fallback to old Delete button for non-SSH scripts */}
|
||||
{(!script.container_id || script.execution_mode !== 'ssh') && (
|
||||
<Button
|
||||
onClick={() => handleDeleteScript(Number(script.id))}
|
||||
variant="delete"
|
||||
size="sm"
|
||||
disabled={deleteScriptMutation.isPending}
|
||||
>
|
||||
{deleteScriptMutation.isPending ? 'Deleting...' : 'Delete'}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -1017,6 +1223,31 @@ export function InstalledScriptsTab() {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Confirmation Modal */}
|
||||
{confirmationModal && (
|
||||
<ConfirmationModal
|
||||
isOpen={confirmationModal.isOpen}
|
||||
onClose={() => setConfirmationModal(null)}
|
||||
onConfirm={confirmationModal.onConfirm}
|
||||
title={confirmationModal.title}
|
||||
message={confirmationModal.message}
|
||||
variant={confirmationModal.variant}
|
||||
confirmText={confirmationModal.confirmText}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Error/Success Modal */}
|
||||
{errorModal && (
|
||||
<ErrorModal
|
||||
isOpen={errorModal.isOpen}
|
||||
onClose={() => setErrorModal(null)}
|
||||
title={errorModal.title}
|
||||
message={errorModal.message}
|
||||
details={errorModal.details}
|
||||
type={errorModal.type ?? 'error'}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ interface InstalledScript {
|
||||
installation_date: string;
|
||||
status: 'in_progress' | 'success' | 'failed';
|
||||
output_log: string | null;
|
||||
execution_mode: 'local' | 'ssh';
|
||||
container_status?: 'running' | 'stopped' | 'unknown';
|
||||
}
|
||||
|
||||
@@ -33,6 +34,11 @@ interface ScriptInstallationCardProps {
|
||||
onDelete: () => void;
|
||||
isUpdating: boolean;
|
||||
isDeleting: boolean;
|
||||
// New container control props
|
||||
containerStatus?: 'running' | 'stopped' | 'unknown';
|
||||
onStartStop: (action: 'start' | 'stop') => void;
|
||||
onDestroy: () => void;
|
||||
isControlling: boolean;
|
||||
}
|
||||
|
||||
export function ScriptInstallationCard({
|
||||
@@ -46,7 +52,11 @@ export function ScriptInstallationCard({
|
||||
onUpdate,
|
||||
onDelete,
|
||||
isUpdating,
|
||||
isDeleting
|
||||
isDeleting,
|
||||
containerStatus,
|
||||
onStartStop,
|
||||
onDestroy,
|
||||
isControlling
|
||||
}: ScriptInstallationCardProps) {
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleString();
|
||||
@@ -188,19 +198,46 @@ export function ScriptInstallationCard({
|
||||
variant="update"
|
||||
size="sm"
|
||||
className="flex-1 min-w-0"
|
||||
disabled={containerStatus === 'stopped'}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={onDelete}
|
||||
variant="delete"
|
||||
size="sm"
|
||||
disabled={isDeleting}
|
||||
className="flex-1 min-w-0"
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : 'Delete'}
|
||||
</Button>
|
||||
{/* Container Control Buttons - only show for SSH scripts with container_id */}
|
||||
{script.container_id && script.execution_mode === 'ssh' && (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => onStartStop(containerStatus === 'running' ? 'stop' : 'start')}
|
||||
disabled={isControlling || containerStatus === 'unknown'}
|
||||
variant={containerStatus === 'running' ? 'destructive' : 'default'}
|
||||
size="sm"
|
||||
className="flex-1 min-w-0"
|
||||
>
|
||||
{isControlling ? 'Working...' : containerStatus === 'running' ? 'Stop' : 'Start'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onDestroy}
|
||||
disabled={isControlling}
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="flex-1 min-w-0"
|
||||
>
|
||||
{isControlling ? 'Working...' : 'Destroy'}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{/* Fallback to old Delete button for non-SSH scripts */}
|
||||
{(!script.container_id || script.execution_mode !== 'ssh') && (
|
||||
<Button
|
||||
onClick={onDelete}
|
||||
variant="delete"
|
||||
size="sm"
|
||||
disabled={isDeleting}
|
||||
className="flex-1 min-w-0"
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : 'Delete'}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -452,7 +452,6 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
||||
successful.push({ slug: slug ?? '', files: result.files ?? [] });
|
||||
} else {
|
||||
const error = 'error' in result ? result.error : 'Failed to load script';
|
||||
console.log(`Script ${slug} failed with error:`, error, 'Full result:', result);
|
||||
const userFriendlyError = getFriendlyErrorMessage(error, slug ?? '');
|
||||
failed.push({ slug: slug ?? '', error: userFriendlyError });
|
||||
}
|
||||
|
||||
@@ -142,7 +142,6 @@ export function VersionDisplay({ onOpenReleaseNotes }: VersionDisplayProps = {})
|
||||
const noLogsForAWhile = timeSinceLastLog > 60000; // 60 seconds
|
||||
|
||||
if (hasBeenUpdatingLongEnough && noLogsForAWhile && isUpdating && !isNetworkError) {
|
||||
console.log('Fallback: Assuming server restart due to long silence');
|
||||
setIsNetworkError(true);
|
||||
setUpdateLogs(prev => [...prev, 'Server restarting... waiting for reconnection...']);
|
||||
|
||||
|
||||
@@ -1,100 +1,8 @@
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
|
||||
import { getDatabase } from "~/server/database";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { getSSHExecutionService } from "~/server/ssh-execution-service";
|
||||
import type { Server } from "~/types/server";
|
||||
// Removed unused imports
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// Helper function to check local container statuses
|
||||
async function getLocalContainerStatuses(containerIds: string[]): Promise<Record<string, 'running' | 'stopped' | 'unknown'>> {
|
||||
try {
|
||||
const { stdout } = await execAsync('pct list');
|
||||
const statusMap: Record<string, 'running' | 'stopped' | 'unknown'> = {};
|
||||
|
||||
// Parse pct list output
|
||||
const lines = stdout.trim().split('\n');
|
||||
const dataLines = lines.slice(1); // Skip header
|
||||
|
||||
for (const line of dataLines) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length >= 2) {
|
||||
const vmid = parts[0];
|
||||
const status = parts[1];
|
||||
|
||||
if (vmid && containerIds.includes(vmid)) {
|
||||
statusMap[vmid] = status === 'running' ? 'running' : 'stopped';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set unknown for containers not found in pct list
|
||||
for (const containerId of containerIds) {
|
||||
if (!(containerId in statusMap)) {
|
||||
statusMap[containerId] = 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
return statusMap;
|
||||
} catch (error) {
|
||||
console.error('Error checking local container statuses:', error);
|
||||
// Return unknown for all containers on error
|
||||
const statusMap: Record<string, 'running' | 'stopped' | 'unknown'> = {};
|
||||
for (const containerId of containerIds) {
|
||||
statusMap[containerId] = 'unknown';
|
||||
}
|
||||
return statusMap;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check remote container statuses (multiple containers per server)
|
||||
async function getRemoteContainerStatuses(containerIds: string[], server: Server): Promise<Record<string, 'running' | 'stopped' | 'unknown'>> {
|
||||
return new Promise((resolve) => {
|
||||
const sshService = getSSHExecutionService();
|
||||
const statusMap: Record<string, 'running' | 'stopped' | 'unknown'> = {};
|
||||
|
||||
// Initialize all containers as unknown
|
||||
for (const containerId of containerIds) {
|
||||
statusMap[containerId] = 'unknown';
|
||||
}
|
||||
|
||||
void sshService.executeCommand(
|
||||
server,
|
||||
'pct list',
|
||||
(data: string) => {
|
||||
// Parse the output to find all containers
|
||||
const lines = data.trim().split('\n');
|
||||
const dataLines = lines.slice(1); // Skip header
|
||||
|
||||
for (const line of dataLines) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length >= 2) {
|
||||
const vmid = parts[0];
|
||||
const status = parts[1];
|
||||
|
||||
// Check if this is one of the containers we're looking for
|
||||
if (vmid && containerIds.includes(vmid)) {
|
||||
statusMap[vmid] = status === 'running' ? 'running' : 'stopped';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolve(statusMap);
|
||||
},
|
||||
(error: string) => {
|
||||
console.error(`Error checking remote containers on server ${server.name}:`, error);
|
||||
resolve(statusMap); // Return the map with unknown statuses
|
||||
},
|
||||
(exitCode: number) => {
|
||||
if (exitCode !== 0) {
|
||||
resolve(statusMap); // Return the map with unknown statuses
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export const installedScriptsRouter = createTRPCRouter({
|
||||
// Get all installed scripts
|
||||
@@ -303,12 +211,8 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
autoDetectLXCContainers: publicProcedure
|
||||
.input(z.object({ serverId: z.number() }))
|
||||
.mutation(async ({ input }) => {
|
||||
console.log('=== AUTO-DETECT API ENDPOINT CALLED ===');
|
||||
console.log('Input received:', input);
|
||||
console.log('Timestamp:', new Date().toISOString());
|
||||
|
||||
try {
|
||||
console.log('Starting auto-detect LXC containers for server ID:', input.serverId);
|
||||
|
||||
const db = getDatabase();
|
||||
const server = db.getServerById(input.serverId);
|
||||
@@ -322,7 +226,6 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
};
|
||||
}
|
||||
|
||||
console.log('Found server:', (server as any).name, 'at', (server as any).ip);
|
||||
|
||||
// Import SSH services
|
||||
const { default: SSHService } = await import('~/server/ssh-service');
|
||||
@@ -331,10 +234,8 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
const sshExecutionService = new SSHExecutionService();
|
||||
|
||||
// Test SSH connection first
|
||||
console.log('Testing SSH connection...');
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
const connectionTest = await sshService.testSSHConnection(server as any);
|
||||
console.log('SSH connection test result:', connectionTest);
|
||||
|
||||
if (!(connectionTest as any).success) {
|
||||
return {
|
||||
@@ -344,14 +245,11 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
};
|
||||
}
|
||||
|
||||
console.log('SSH connection successful, scanning for LXC containers...');
|
||||
|
||||
// Use the working approach - manual loop through all config files
|
||||
const command = `for file in /etc/pve/lxc/*.conf; do if [ -f "$file" ]; then if grep -q "community-script" "$file"; then echo "$file"; fi; fi; done`;
|
||||
let detectedContainers: any[] = [];
|
||||
|
||||
console.log('Executing manual loop command...');
|
||||
console.log('Command:', command);
|
||||
|
||||
let commandOutput = '';
|
||||
|
||||
@@ -367,8 +265,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
(error: string) => {
|
||||
console.error('Command error:', error);
|
||||
},
|
||||
(exitCode: number) => {
|
||||
console.log('Command exit code:', exitCode);
|
||||
(_exitCode: number) => {
|
||||
|
||||
// Parse the complete output to get config file paths that contain community-script tag
|
||||
const configFiles = commandOutput.split('\n')
|
||||
@@ -376,8 +273,6 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
.map((line: string) => line.trim())
|
||||
.filter((line: string) => line.endsWith('.conf'));
|
||||
|
||||
console.log('Found config files with community-script tag:', configFiles.length);
|
||||
console.log('Config files:', configFiles);
|
||||
|
||||
// Process each config file to extract hostname
|
||||
const processPromises = configFiles.map(async (configPath: string) => {
|
||||
@@ -385,7 +280,6 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
const containerId = configPath.split('/').pop()?.replace('.conf', '');
|
||||
if (!containerId) return null;
|
||||
|
||||
console.log('Processing container:', containerId, 'from', configPath);
|
||||
|
||||
// Read the config file content
|
||||
const readCommand = `cat "${configPath}" 2>/dev/null`;
|
||||
@@ -415,13 +309,11 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
containerId,
|
||||
hostname,
|
||||
configPath,
|
||||
serverId: (server as any).id,
|
||||
serverId: Number((server as any).id),
|
||||
serverName: (server as any).name
|
||||
};
|
||||
console.log('Adding container to detected list:', container);
|
||||
readResolve(container);
|
||||
} else {
|
||||
console.log('No hostname found for', containerId);
|
||||
readResolve(null);
|
||||
}
|
||||
},
|
||||
@@ -443,7 +335,6 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
// Wait for all config files to be processed
|
||||
void Promise.all(processPromises).then((results) => {
|
||||
detectedContainers = results.filter(result => result !== null);
|
||||
console.log('Final detected containers:', detectedContainers.length);
|
||||
resolve();
|
||||
}).catch((error) => {
|
||||
console.error('Error processing config files:', error);
|
||||
@@ -453,7 +344,6 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
);
|
||||
});
|
||||
|
||||
console.log('Detected containers:', detectedContainers.length);
|
||||
|
||||
// Get existing scripts to check for duplicates
|
||||
const existingScripts = db.getAllInstalledScripts();
|
||||
@@ -471,7 +361,6 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
);
|
||||
|
||||
if (duplicate) {
|
||||
console.log(`Skipping duplicate: ${container.hostname} (${container.containerId}) already exists`);
|
||||
skippedScripts.push({
|
||||
containerId: container.containerId,
|
||||
hostname: container.hostname,
|
||||
@@ -480,7 +369,6 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log('Creating script record for:', container.hostname, container.containerId);
|
||||
const result = db.createInstalledScript({
|
||||
script_name: container.hostname,
|
||||
script_path: `detected/${container.hostname}`,
|
||||
@@ -497,7 +385,6 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
hostname: container.hostname,
|
||||
serverName: container.serverName
|
||||
});
|
||||
console.log('Created script record with ID:', result.lastInsertRowid);
|
||||
} catch (error) {
|
||||
console.error(`Error creating script record for ${container.hostname}:`, error);
|
||||
}
|
||||
@@ -527,15 +414,11 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
cleanupOrphanedScripts: publicProcedure
|
||||
.mutation(async () => {
|
||||
try {
|
||||
console.log('=== CLEANUP ORPHANED SCRIPTS API ENDPOINT CALLED ===');
|
||||
console.log('Timestamp:', new Date().toISOString());
|
||||
|
||||
const db = getDatabase();
|
||||
const allScripts = db.getAllInstalledScripts();
|
||||
const allServers = db.getAllServers();
|
||||
|
||||
console.log('Found scripts:', allScripts.length);
|
||||
console.log('Found servers:', allServers.length);
|
||||
|
||||
if (allScripts.length === 0) {
|
||||
return {
|
||||
@@ -559,26 +442,22 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
script.container_id
|
||||
);
|
||||
|
||||
console.log('Scripts to check for cleanup:', scriptsToCheck.length);
|
||||
|
||||
for (const script of scriptsToCheck) {
|
||||
try {
|
||||
const scriptData = script as any;
|
||||
const server = allServers.find((s: any) => s.id === scriptData.server_id);
|
||||
if (!server) {
|
||||
console.log(`Server not found for script ${scriptData.script_name}, marking for deletion`);
|
||||
db.deleteInstalledScript(Number(scriptData.id));
|
||||
deletedScripts.push(String(scriptData.script_name));
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`Checking script ${scriptData.script_name} on server ${(server as any).name}`);
|
||||
|
||||
// Test SSH connection
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
const connectionTest = await sshService.testSSHConnection(server as any);
|
||||
if (!(connectionTest as any).success) {
|
||||
console.log(`SSH connection failed for server ${(server as any).name}, skipping script ${scriptData.script_name}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -605,11 +484,9 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
});
|
||||
|
||||
if (!containerExists) {
|
||||
console.log(`Container ${scriptData.container_id} not found on server ${(server as any).name}, deleting script ${scriptData.script_name}`);
|
||||
db.deleteInstalledScript(Number(scriptData.id));
|
||||
deletedScripts.push(String(scriptData.script_name));
|
||||
} else {
|
||||
console.log(`Container ${scriptData.container_id} still exists on server ${(server as any).name}, keeping script ${scriptData.script_name}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
@@ -617,7 +494,6 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Cleanup completed. Deleted scripts:', deletedScripts);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -639,78 +515,89 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
// Get container running statuses
|
||||
getContainerStatuses: publicProcedure
|
||||
.input(z.object({
|
||||
containers: z.array(z.object({
|
||||
containerId: z.string(),
|
||||
serverId: z.number().optional(),
|
||||
server: z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
ip: z.string(),
|
||||
user: z.string(),
|
||||
password: z.string(),
|
||||
auth_type: z.string()
|
||||
}).optional()
|
||||
}))
|
||||
serverIds: z.array(z.number()).optional() // Optional: check specific servers, or all if empty
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
const { containers } = input;
|
||||
|
||||
const db = getDatabase();
|
||||
const allServers = db.getAllServers();
|
||||
const statusMap: Record<string, 'running' | 'stopped' | 'unknown'> = {};
|
||||
|
||||
// Group containers by server (local vs remote)
|
||||
const localContainers: string[] = [];
|
||||
const remoteContainers: Array<{containerId: string, server: any}> = [];
|
||||
|
||||
for (const container of containers) {
|
||||
if (!container.serverId || !container.server) {
|
||||
localContainers.push(container.containerId);
|
||||
} else {
|
||||
remoteContainers.push({
|
||||
containerId: container.containerId,
|
||||
server: container.server
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check local containers
|
||||
if (localContainers.length > 0) {
|
||||
const localStatuses = await getLocalContainerStatuses(localContainers);
|
||||
Object.assign(statusMap, localStatuses);
|
||||
}
|
||||
|
||||
// Check remote containers - group by server and make one call per server
|
||||
const serverGroups: Record<string, Array<{containerId: string, server: any}>> = {};
|
||||
|
||||
for (const { containerId, server } of remoteContainers) {
|
||||
const serverKey = `${server.id}-${server.name}`;
|
||||
serverGroups[serverKey] ??= [];
|
||||
serverGroups[serverKey].push({ containerId, server });
|
||||
}
|
||||
|
||||
// Make one call per server
|
||||
for (const [serverKey, containers] of Object.entries(serverGroups)) {
|
||||
|
||||
// Import SSH services
|
||||
const { default: SSHService } = await import('~/server/ssh-service');
|
||||
const { default: SSHExecutionService } = await import('~/server/ssh-execution-service');
|
||||
const sshService = new SSHService();
|
||||
const sshExecutionService = new SSHExecutionService();
|
||||
|
||||
// Determine which servers to check
|
||||
const serversToCheck = input.serverIds
|
||||
? allServers.filter((s: any) => input.serverIds!.includes(Number(s.id)))
|
||||
: allServers;
|
||||
|
||||
|
||||
// Check status for each server
|
||||
for (const server of serversToCheck) {
|
||||
try {
|
||||
if (containers.length === 0) continue;
|
||||
|
||||
// Test SSH connection
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
const connectionTest = await sshService.testSSHConnection(server as any);
|
||||
if (!(connectionTest as any).success) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Run pct list to get all container statuses at once
|
||||
const listCommand = 'pct list';
|
||||
let listOutput = '';
|
||||
|
||||
const server = containers[0]?.server;
|
||||
if (!server) continue;
|
||||
|
||||
const containerIds = containers.map(c => c.containerId).filter(Boolean);
|
||||
const serverStatuses = await getRemoteContainerStatuses(containerIds, server as Server);
|
||||
|
||||
// Merge the results
|
||||
Object.assign(statusMap, serverStatuses);
|
||||
} catch (error) {
|
||||
console.error(`Error checking statuses for server ${serverKey}:`, error);
|
||||
// Set all containers for this server to unknown
|
||||
for (const container of containers) {
|
||||
if (container.containerId) {
|
||||
statusMap[container.containerId] = 'unknown';
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
void sshExecutionService.executeCommand(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
server as any,
|
||||
listCommand,
|
||||
(data: string) => {
|
||||
listOutput += data;
|
||||
},
|
||||
(error: string) => {
|
||||
console.error(`pct list error on server ${(server as any).name}:`, error);
|
||||
reject(new Error(error));
|
||||
},
|
||||
(_exitCode: number) => {
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Parse pct list output
|
||||
const lines = listOutput.split('\n').filter(line => line.trim());
|
||||
for (const line of lines) {
|
||||
// pct list format: CTID Status Name
|
||||
// Example: "100 running my-container"
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length >= 3) {
|
||||
const containerId = parts[0];
|
||||
const status = parts[1];
|
||||
|
||||
if (containerId && status) {
|
||||
// Map pct list status to our status
|
||||
let mappedStatus: 'running' | 'stopped' | 'unknown' = 'unknown';
|
||||
if (status === 'running') {
|
||||
mappedStatus = 'running';
|
||||
} else if (status === 'stopped') {
|
||||
mappedStatus = 'stopped';
|
||||
}
|
||||
|
||||
statusMap[containerId] = mappedStatus;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing server ${(server as any).name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
statusMap
|
||||
@@ -723,5 +610,360 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
statusMap: {}
|
||||
};
|
||||
}
|
||||
}),
|
||||
|
||||
// Get container status (running/stopped)
|
||||
getContainerStatus: publicProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.query(async ({ input }) => {
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const script = db.getInstalledScriptById(input.id);
|
||||
|
||||
if (!script) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Script not found',
|
||||
status: 'unknown' as const
|
||||
};
|
||||
}
|
||||
|
||||
const scriptData = script as any;
|
||||
|
||||
// Only check status for SSH scripts with container_id
|
||||
if (scriptData.execution_mode !== 'ssh' || !scriptData.server_id || !scriptData.container_id) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Script is not an SSH script with container ID',
|
||||
status: 'unknown' as const
|
||||
};
|
||||
}
|
||||
|
||||
// Get server info
|
||||
const server = db.getServerById(Number(scriptData.server_id));
|
||||
if (!server) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Server not found',
|
||||
status: 'unknown' as const
|
||||
};
|
||||
}
|
||||
|
||||
// Import SSH services
|
||||
const { default: SSHService } = await import('~/server/ssh-service');
|
||||
const { default: SSHExecutionService } = await import('~/server/ssh-execution-service');
|
||||
const sshService = new SSHService();
|
||||
const sshExecutionService = new SSHExecutionService();
|
||||
|
||||
// Test SSH connection first
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
const connectionTest = await sshService.testSSHConnection(server as any);
|
||||
if (!(connectionTest as any).success) {
|
||||
return {
|
||||
success: false,
|
||||
error: `SSH connection failed: ${(connectionTest as any).error ?? 'Unknown error'}`,
|
||||
status: 'unknown' as const
|
||||
};
|
||||
}
|
||||
|
||||
// Check container status
|
||||
const statusCommand = `pct status ${scriptData.container_id}`;
|
||||
let statusOutput = '';
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
void sshExecutionService.executeCommand(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
server as any,
|
||||
statusCommand,
|
||||
(data: string) => {
|
||||
statusOutput += data;
|
||||
},
|
||||
(error: string) => {
|
||||
console.error('Status command error:', error);
|
||||
reject(new Error(error));
|
||||
},
|
||||
(_exitCode: number) => {
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Parse status from output
|
||||
let status: 'running' | 'stopped' | 'unknown' = 'unknown';
|
||||
if (statusOutput.includes('status: running')) {
|
||||
status = 'running';
|
||||
} else if (statusOutput.includes('status: stopped')) {
|
||||
status = 'stopped';
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status,
|
||||
error: undefined
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error in getContainerStatus:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to get container status',
|
||||
status: 'unknown' as const
|
||||
};
|
||||
}
|
||||
}),
|
||||
|
||||
// Control container (start/stop)
|
||||
controlContainer: publicProcedure
|
||||
.input(z.object({
|
||||
id: z.number(),
|
||||
action: z.enum(['start', 'stop'])
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const script = db.getInstalledScriptById(input.id);
|
||||
|
||||
if (!script) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Script not found'
|
||||
};
|
||||
}
|
||||
|
||||
const scriptData = script as any;
|
||||
|
||||
// Only control SSH scripts with container_id
|
||||
if (scriptData.execution_mode !== 'ssh' || !scriptData.server_id || !scriptData.container_id) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Script is not an SSH script with container ID'
|
||||
};
|
||||
}
|
||||
|
||||
// Get server info
|
||||
const server = db.getServerById(Number(scriptData.server_id));
|
||||
if (!server) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Server not found'
|
||||
};
|
||||
}
|
||||
|
||||
// Import SSH services
|
||||
const { default: SSHService } = await import('~/server/ssh-service');
|
||||
const { default: SSHExecutionService } = await import('~/server/ssh-execution-service');
|
||||
const sshService = new SSHService();
|
||||
const sshExecutionService = new SSHExecutionService();
|
||||
|
||||
// Test SSH connection first
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
const connectionTest = await sshService.testSSHConnection(server as any);
|
||||
if (!(connectionTest as any).success) {
|
||||
return {
|
||||
success: false,
|
||||
error: `SSH connection failed: ${(connectionTest as any).error ?? 'Unknown error'}`
|
||||
};
|
||||
}
|
||||
|
||||
// Execute control command
|
||||
const controlCommand = `pct ${input.action} ${scriptData.container_id}`;
|
||||
let commandOutput = '';
|
||||
let commandError = '';
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
void sshExecutionService.executeCommand(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
server as any,
|
||||
controlCommand,
|
||||
(data: string) => {
|
||||
commandOutput += data;
|
||||
},
|
||||
(error: string) => {
|
||||
commandError += error;
|
||||
},
|
||||
(exitCode: number) => {
|
||||
if (exitCode !== 0) {
|
||||
const errorMessage = commandError || commandOutput || `Command failed with exit code ${exitCode}`;
|
||||
reject(new Error(errorMessage));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Container ${scriptData.container_id} ${input.action} command executed successfully`,
|
||||
containerId: scriptData.container_id
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error in controlContainer:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to control container'
|
||||
};
|
||||
}
|
||||
}),
|
||||
|
||||
// Destroy container and delete DB record
|
||||
destroyContainer: publicProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const script = db.getInstalledScriptById(input.id);
|
||||
|
||||
if (!script) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Script not found'
|
||||
};
|
||||
}
|
||||
|
||||
const scriptData = script as any;
|
||||
|
||||
// Only destroy SSH scripts with container_id
|
||||
if (scriptData.execution_mode !== 'ssh' || !scriptData.server_id || !scriptData.container_id) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Script is not an SSH script with container ID'
|
||||
};
|
||||
}
|
||||
|
||||
// Get server info
|
||||
const server = db.getServerById(Number(scriptData.server_id));
|
||||
if (!server) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Server not found'
|
||||
};
|
||||
}
|
||||
|
||||
// Import SSH services
|
||||
const { default: SSHService } = await import('~/server/ssh-service');
|
||||
const { default: SSHExecutionService } = await import('~/server/ssh-execution-service');
|
||||
const sshService = new SSHService();
|
||||
const sshExecutionService = new SSHExecutionService();
|
||||
|
||||
// Test SSH connection first
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
const connectionTest = await sshService.testSSHConnection(server as any);
|
||||
if (!(connectionTest as any).success) {
|
||||
return {
|
||||
success: false,
|
||||
error: `SSH connection failed: ${(connectionTest as any).error ?? 'Unknown error'}`
|
||||
};
|
||||
}
|
||||
|
||||
// First check if container is running and stop it if necessary
|
||||
const statusCommand = `pct status ${scriptData.container_id}`;
|
||||
let statusOutput = '';
|
||||
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
void sshExecutionService.executeCommand(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
server as any,
|
||||
statusCommand,
|
||||
(data: string) => {
|
||||
statusOutput += data;
|
||||
},
|
||||
(error: string) => {
|
||||
reject(new Error(error));
|
||||
},
|
||||
(_exitCode: number) => {
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Check if container is running
|
||||
if (statusOutput.includes('status: running')) {
|
||||
// Stop the container first
|
||||
const stopCommand = `pct stop ${scriptData.container_id}`;
|
||||
let stopOutput = '';
|
||||
let stopError = '';
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
void sshExecutionService.executeCommand(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
server as any,
|
||||
stopCommand,
|
||||
(data: string) => {
|
||||
stopOutput += data;
|
||||
},
|
||||
(error: string) => {
|
||||
stopError += error;
|
||||
},
|
||||
(exitCode: number) => {
|
||||
if (exitCode !== 0) {
|
||||
const errorMessage = stopError || stopOutput || `Stop command failed with exit code ${exitCode}`;
|
||||
reject(new Error(`Failed to stop container: ${errorMessage}`));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
} catch (_error) {
|
||||
// If status check fails, continue with destroy attempt
|
||||
// The destroy command will handle the error appropriately
|
||||
}
|
||||
|
||||
// Execute destroy command
|
||||
const destroyCommand = `pct destroy ${scriptData.container_id}`;
|
||||
let commandOutput = '';
|
||||
let commandError = '';
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
void sshExecutionService.executeCommand(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
server as any,
|
||||
destroyCommand,
|
||||
(data: string) => {
|
||||
commandOutput += data;
|
||||
},
|
||||
(error: string) => {
|
||||
commandError += error;
|
||||
},
|
||||
(exitCode: number) => {
|
||||
if (exitCode !== 0) {
|
||||
const errorMessage = commandError || commandOutput || `Destroy command failed with exit code ${exitCode}`;
|
||||
reject(new Error(errorMessage));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// If destroy was successful, delete the database record
|
||||
const deleteResult = db.deleteInstalledScript(input.id);
|
||||
|
||||
if (deleteResult.changes === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Container destroyed but failed to delete database record'
|
||||
};
|
||||
}
|
||||
|
||||
// Determine if container was stopped first
|
||||
const wasStopped = statusOutput.includes('status: running');
|
||||
const message = wasStopped
|
||||
? `Container ${scriptData.container_id} stopped and destroyed successfully, database record deleted`
|
||||
: `Container ${scriptData.container_id} destroyed successfully, database record deleted`;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error in destroyContainer:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to destroy container'
|
||||
};
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user