* 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
307 lines
11 KiB
TypeScript
307 lines
11 KiB
TypeScript
'use client';
|
|
|
|
import { api } from "~/trpc/react";
|
|
import { Badge } from "./ui/badge";
|
|
import { Button } from "./ui/button";
|
|
import { ContextualHelpIcon } from "./ContextualHelpIcon";
|
|
|
|
import { ExternalLink, Download, RefreshCw, Loader2 } from "lucide-react";
|
|
import { useState, useEffect, useRef } from "react";
|
|
|
|
interface VersionDisplayProps {
|
|
onOpenReleaseNotes?: () => void;
|
|
}
|
|
|
|
// Loading overlay component with log streaming
|
|
function LoadingOverlay({
|
|
isNetworkError = false,
|
|
logs = []
|
|
}: {
|
|
isNetworkError?: boolean;
|
|
logs?: string[];
|
|
}) {
|
|
const logsEndRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Auto-scroll to bottom when new logs arrive
|
|
useEffect(() => {
|
|
logsEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
}, [logs]);
|
|
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
|
<div className="bg-card rounded-lg p-8 shadow-2xl border border-border max-w-2xl w-full mx-4 max-h-[80vh] flex flex-col">
|
|
<div className="flex flex-col items-center space-y-4">
|
|
<div className="relative">
|
|
<Loader2 className="h-12 w-12 animate-spin text-primary" />
|
|
<div className="absolute inset-0 rounded-full border-2 border-primary/20 animate-pulse"></div>
|
|
</div>
|
|
<div className="text-center">
|
|
<h3 className="text-lg font-semibold text-card-foreground mb-2">
|
|
{isNetworkError ? 'Server Restarting' : 'Updating Application'}
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
{isNetworkError
|
|
? 'The server is restarting after the update...'
|
|
: 'Please stand by while we update your application...'
|
|
}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground mt-2">
|
|
{isNetworkError
|
|
? 'This may take a few moments. The page will reload automatically.'
|
|
: 'The server will restart automatically when complete.'
|
|
}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Log output */}
|
|
{logs.length > 0 && (
|
|
<div className="w-full mt-4 bg-card border border-border rounded-lg p-4 font-mono text-xs text-chart-2 max-h-60 overflow-y-auto terminal-output">
|
|
{logs.map((log, index) => (
|
|
<div key={index} className="mb-1 whitespace-pre-wrap break-words">
|
|
{log}
|
|
</div>
|
|
))}
|
|
<div ref={logsEndRef} />
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex space-x-1">
|
|
<div className="w-2 h-2 bg-primary rounded-full animate-bounce"></div>
|
|
<div className="w-2 h-2 bg-primary rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
|
|
<div className="w-2 h-2 bg-primary rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function VersionDisplay({ onOpenReleaseNotes }: VersionDisplayProps = {}) {
|
|
const { data: versionStatus, isLoading, error } = api.version.getVersionStatus.useQuery();
|
|
const [isUpdating, setIsUpdating] = useState(false);
|
|
const [updateResult, setUpdateResult] = useState<{ success: boolean; message: string } | null>(null);
|
|
const [isNetworkError, setIsNetworkError] = useState(false);
|
|
const [updateLogs, setUpdateLogs] = useState<string[]>([]);
|
|
const [shouldSubscribe, setShouldSubscribe] = useState(false);
|
|
const [updateStartTime, setUpdateStartTime] = useState<number | null>(null);
|
|
const lastLogTimeRef = useRef<number>(Date.now());
|
|
const reconnectIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
const executeUpdate = api.version.executeUpdate.useMutation({
|
|
onSuccess: (result) => {
|
|
setUpdateResult({ success: result.success, message: result.message });
|
|
|
|
if (result.success) {
|
|
// Start subscribing to update logs
|
|
setShouldSubscribe(true);
|
|
setUpdateLogs(['Update started...']);
|
|
} else {
|
|
setIsUpdating(false);
|
|
}
|
|
},
|
|
onError: (error) => {
|
|
setUpdateResult({ success: false, message: error.message });
|
|
setIsUpdating(false);
|
|
}
|
|
});
|
|
|
|
// Poll for update logs
|
|
const { data: updateLogsData } = api.version.getUpdateLogs.useQuery(undefined, {
|
|
enabled: shouldSubscribe,
|
|
refetchInterval: 1000, // Poll every second
|
|
refetchIntervalInBackground: true,
|
|
});
|
|
|
|
// Update logs when data changes
|
|
useEffect(() => {
|
|
if (updateLogsData?.success && updateLogsData.logs) {
|
|
lastLogTimeRef.current = Date.now();
|
|
setUpdateLogs(updateLogsData.logs);
|
|
|
|
if (updateLogsData.isComplete) {
|
|
setUpdateLogs(prev => [...prev, 'Update complete! Server restarting...']);
|
|
setIsNetworkError(true);
|
|
// Start reconnection attempts when we know update is complete
|
|
startReconnectAttempts();
|
|
}
|
|
}
|
|
}, [updateLogsData]);
|
|
|
|
// Monitor for server connection loss and auto-reload (fallback only)
|
|
useEffect(() => {
|
|
if (!shouldSubscribe) return;
|
|
|
|
// Only use this as a fallback - the main trigger should be completion detection
|
|
const checkInterval = setInterval(() => {
|
|
const timeSinceLastLog = Date.now() - lastLogTimeRef.current;
|
|
|
|
// Only start reconnection if we've been updating for at least 3 minutes
|
|
// and no logs for 60 seconds (very conservative fallback)
|
|
const hasBeenUpdatingLongEnough = updateStartTime && (Date.now() - updateStartTime) > 180000; // 3 minutes
|
|
const noLogsForAWhile = timeSinceLastLog > 60000; // 60 seconds
|
|
|
|
if (hasBeenUpdatingLongEnough && noLogsForAWhile && isUpdating && !isNetworkError) {
|
|
setIsNetworkError(true);
|
|
setUpdateLogs(prev => [...prev, 'Server restarting... waiting for reconnection...']);
|
|
|
|
// Start trying to reconnect
|
|
startReconnectAttempts();
|
|
}
|
|
}, 10000); // Check every 10 seconds
|
|
|
|
return () => clearInterval(checkInterval);
|
|
}, [shouldSubscribe, isUpdating, updateStartTime, isNetworkError]);
|
|
|
|
// Attempt to reconnect and reload page when server is back
|
|
const startReconnectAttempts = () => {
|
|
if (reconnectIntervalRef.current) return;
|
|
|
|
setUpdateLogs(prev => [...prev, 'Attempting to reconnect...']);
|
|
|
|
reconnectIntervalRef.current = setInterval(() => {
|
|
void (async () => {
|
|
try {
|
|
// Try to fetch the root path to check if server is back
|
|
const response = await fetch('/', { method: 'HEAD' });
|
|
if (response.ok || response.status === 200) {
|
|
setUpdateLogs(prev => [...prev, 'Server is back online! Reloading...']);
|
|
|
|
// Clear interval and reload
|
|
if (reconnectIntervalRef.current) {
|
|
clearInterval(reconnectIntervalRef.current);
|
|
}
|
|
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 1000);
|
|
}
|
|
} catch {
|
|
// Server still down, keep trying
|
|
}
|
|
})();
|
|
}, 2000);
|
|
};
|
|
|
|
// Cleanup reconnect interval on unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
if (reconnectIntervalRef.current) {
|
|
clearInterval(reconnectIntervalRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
const handleUpdate = () => {
|
|
setIsUpdating(true);
|
|
setUpdateResult(null);
|
|
setIsNetworkError(false);
|
|
setUpdateLogs([]);
|
|
setShouldSubscribe(false);
|
|
setUpdateStartTime(Date.now());
|
|
lastLogTimeRef.current = Date.now();
|
|
executeUpdate.mutate();
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<Badge variant="secondary" className="animate-pulse">
|
|
Loading...
|
|
</Badge>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error || !versionStatus?.success) {
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<Badge variant="destructive">
|
|
v{versionStatus?.currentVersion ?? 'Unknown'}
|
|
</Badge>
|
|
<span className="text-xs text-muted-foreground">
|
|
(Unable to check for updates)
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const { currentVersion, isUpToDate, updateAvailable, releaseInfo } = versionStatus;
|
|
|
|
return (
|
|
<>
|
|
{/* Loading overlay */}
|
|
{isUpdating && <LoadingOverlay isNetworkError={isNetworkError} logs={updateLogs} />}
|
|
|
|
<div className="flex flex-col sm:flex-row items-center gap-2 sm:gap-2">
|
|
<Badge
|
|
variant={isUpToDate ? "default" : "secondary"}
|
|
className={`text-xs ${onOpenReleaseNotes ? 'cursor-pointer hover:opacity-80 transition-opacity' : ''}`}
|
|
onClick={onOpenReleaseNotes}
|
|
>
|
|
v{currentVersion}
|
|
</Badge>
|
|
|
|
{updateAvailable && releaseInfo && (
|
|
<div className="flex flex-col sm:flex-row items-center gap-2 sm:gap-3">
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
onClick={handleUpdate}
|
|
disabled={isUpdating}
|
|
size="sm"
|
|
variant="destructive"
|
|
className="text-xs h-6 px-2"
|
|
>
|
|
{isUpdating ? (
|
|
<>
|
|
<RefreshCw className="h-3 w-3 mr-1 animate-spin" />
|
|
<span className="hidden sm:inline">Updating...</span>
|
|
<span className="sm:hidden">...</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Download className="h-3 w-3 mr-1" />
|
|
<span className="hidden sm:inline">Update Now</span>
|
|
<span className="sm:hidden">Update</span>
|
|
</>
|
|
)}
|
|
</Button>
|
|
|
|
<ContextualHelpIcon section="update-system" tooltip="Help with updates" />
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1">
|
|
<span className="text-xs text-muted-foreground">Release Notes:</span>
|
|
<a
|
|
href={releaseInfo.htmlUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
title="View latest release"
|
|
>
|
|
<ExternalLink className="h-3 w-3" />
|
|
</a>
|
|
</div>
|
|
|
|
{updateResult && (
|
|
<div className={`text-xs px-2 py-1 rounded text-center ${
|
|
updateResult.success
|
|
? 'bg-chart-2/20 text-chart-2 border border-chart-2/30'
|
|
: 'bg-destructive/20 text-destructive border border-destructive/30'
|
|
}`}>
|
|
{updateResult.message}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{isUpToDate && (
|
|
<span className="text-xs text-chart-2">
|
|
✓ Up to date
|
|
</span>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|