diff --git a/VERSION b/VERSION
index 17e51c3..6e8bf73 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.1.1
+0.1.0
diff --git a/package-lock.json b/package-lock.json
index f9d446c..dfc0d47 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,6 +22,7 @@
"better-sqlite3": "^9.6.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "lucide-react": "^0.545.0",
"next": "^15.5.3",
"node-pty": "^1.0.0",
"react": "^19.0.0",
@@ -7369,6 +7370,15 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lucide-react": {
+ "version": "0.545.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.545.0.tgz",
+ "integrity": "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
diff --git a/package.json b/package.json
index 45cfab9..930192b 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,7 @@
"better-sqlite3": "^9.6.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "lucide-react": "^0.545.0",
"next": "^15.5.3",
"node-pty": "^1.0.0",
"react": "^19.0.0",
diff --git a/src/app/_components/VersionDisplay.tsx b/src/app/_components/VersionDisplay.tsx
new file mode 100644
index 0000000..68deef7
--- /dev/null
+++ b/src/app/_components/VersionDisplay.tsx
@@ -0,0 +1,233 @@
+'use client';
+
+import { api } from "~/trpc/react";
+import { Badge } from "./ui/badge";
+import { Button } from "./ui/button";
+import { ExternalLink, Download, RefreshCw, Loader2 } from "lucide-react";
+import { useState } from "react";
+
+// Loading overlay component
+function LoadingOverlay({ isNetworkError = false }: { isNetworkError?: boolean }) {
+ return (
+
+
+
+
+
+
+ {isNetworkError ? 'Server Restarting' : 'Updating Application'}
+
+
+ {isNetworkError
+ ? 'The server is restarting after the update...'
+ : 'Please stand by while we update your application...'
+ }
+
+
+ {isNetworkError
+ ? 'This may take a few moments. The page will reload automatically. You may see a blank page for up to a minute!.'
+ : 'The server will restart automatically when complete.'
+ }
+
+
+
+
+
+
+ );
+}
+
+export function VersionDisplay() {
+ 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 [updateStartTime, setUpdateStartTime] = useState(null);
+ const [isNetworkError, setIsNetworkError] = useState(false);
+
+ const executeUpdate = api.version.executeUpdate.useMutation({
+ onSuccess: (result: any) => {
+ const now = Date.now();
+ const elapsed = updateStartTime ? now - updateStartTime : 0;
+
+
+ setUpdateResult({ success: result.success, message: result.message });
+
+ if (result.success) {
+ // The script now runs independently, so we show a longer overlay
+ // and wait for the server to restart
+ setIsNetworkError(true);
+ setUpdateResult({ success: true, message: 'Update in progress... Server will restart automatically.' });
+
+ // Wait longer for the update to complete and server to restart
+ setTimeout(() => {
+ setIsUpdating(false);
+ setIsNetworkError(false);
+ // Try to reload after the update completes
+ setTimeout(() => {
+ window.location.reload();
+ }, 10000); // 10 seconds to allow for update completion
+ }, 5000); // Show overlay for 5 seconds
+ } else {
+ // For errors, show for at least 1 second
+ const remainingTime = Math.max(0, 1000 - elapsed);
+ setTimeout(() => {
+ setIsUpdating(false);
+ }, remainingTime);
+ }
+ },
+ onError: (error) => {
+ const now = Date.now();
+ const elapsed = updateStartTime ? now - updateStartTime : 0;
+
+ // Check if this is a network error (expected during server restart)
+ const isNetworkError = error.message.includes('Failed to fetch') ||
+ error.message.includes('NetworkError') ||
+ error.message.includes('fetch') ||
+ error.message.includes('network');
+
+ if (isNetworkError && elapsed < 60000) { // If it's a network error within 30 seconds, treat as success
+ setIsNetworkError(true);
+ setUpdateResult({ success: true, message: 'Update in progress... Server is restarting.' });
+
+ // Wait longer for server to come back up
+ setTimeout(() => {
+ setIsUpdating(false);
+ setIsNetworkError(false);
+ // Try to reload after a longer delay
+ setTimeout(() => {
+ window.location.reload();
+ }, 5000);
+ }, 3000);
+ } else {
+ // For real errors, show for at least 1 second
+ setUpdateResult({ success: false, message: error.message });
+ const remainingTime = Math.max(0, 1000 - elapsed);
+ setTimeout(() => {
+ setIsUpdating(false);
+ }, remainingTime);
+ }
+ }
+ });
+
+ const handleUpdate = () => {
+ setIsUpdating(true);
+ setUpdateResult(null);
+ setIsNetworkError(false);
+ setUpdateStartTime(Date.now());
+ executeUpdate.mutate();
+ };
+
+ if (isLoading) {
+ return (
+
+
+ Loading...
+
+
+ );
+ }
+
+ if (error || !versionStatus?.success) {
+ return (
+
+
+ v{versionStatus?.currentVersion ?? 'Unknown'}
+
+
+ (Unable to check for updates)
+
+
+ );
+ }
+
+ const { currentVersion, isUpToDate, updateAvailable, releaseInfo } = versionStatus;
+
+ return (
+ <>
+ {/* Loading overlay */}
+ {isUpdating && }
+
+
+
+ v{currentVersion}
+
+
+ {updateAvailable && releaseInfo && (
+
+
+
+ Update Available
+
+
+
+
How to update:
+
Click the button to update
+
or update manually:
+
cd $PVESCRIPTLOCAL_DIR
+
git pull
+
npm install
+
npm run build
+
npm start
+
+
+
+
+
+
+ {isUpdating ? (
+ <>
+
+ Updating...
+ >
+ ) : (
+ <>
+
+ Update Now
+ >
+ )}
+
+
+
+
+
+
+ {updateResult && (
+
+ {updateResult.message}
+
+ )}
+
+ )}
+
+ {isUpToDate && (
+
+ ✓ Up to date
+
+ )}
+
+ >
+ );
+}
diff --git a/src/app/_components/ui/badge.tsx b/src/app/_components/ui/badge.tsx
new file mode 100644
index 0000000..cae6814
--- /dev/null
+++ b/src/app/_components/ui/badge.tsx
@@ -0,0 +1,28 @@
+import * as React from "react"
+import { cn } from "~/lib/utils"
+
+export interface BadgeProps extends React.HTMLAttributes {
+ variant?: "default" | "secondary" | "destructive" | "outline"
+}
+
+function Badge({ className, variant = "default", ...props }: BadgeProps) {
+ const variantClasses = {
+ default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+ secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
+ outline: "text-foreground",
+ }
+
+ return (
+
+ )
+}
+
+export { Badge }
diff --git a/src/app/page.tsx b/src/app/page.tsx
index dbd8a17..b1298ef 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -8,6 +8,7 @@ import { InstalledScriptsTab } from './_components/InstalledScriptsTab';
import { ResyncButton } from './_components/ResyncButton';
import { Terminal } from './_components/Terminal';
import { SettingsButton } from './_components/SettingsButton';
+import { VersionDisplay } from './_components/VersionDisplay';
import { Button } from './_components/ui/button';
export default function Home() {
@@ -30,9 +31,12 @@ export default function Home() {
🚀 PVE Scripts Management
-
+
Manage and execute Proxmox helper scripts locally with live output streaming
+
+
+
{/* Controls */}
diff --git a/src/server/api/root.ts b/src/server/api/root.ts
index 833f334..2b3a3fc 100644
--- a/src/server/api/root.ts
+++ b/src/server/api/root.ts
@@ -1,6 +1,7 @@
import { scriptsRouter } from "~/server/api/routers/scripts";
import { installedScriptsRouter } from "~/server/api/routers/installedScripts";
import { serversRouter } from "~/server/api/routers/servers";
+import { versionRouter } from "~/server/api/routers/version";
import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc";
/**
@@ -12,6 +13,7 @@ export const appRouter = createTRPCRouter({
scripts: scriptsRouter,
installedScripts: installedScriptsRouter,
servers: serversRouter,
+ version: versionRouter,
});
// export type definition of API
diff --git a/src/server/api/routers/version.ts b/src/server/api/routers/version.ts
new file mode 100644
index 0000000..9aa0a70
--- /dev/null
+++ b/src/server/api/routers/version.ts
@@ -0,0 +1,148 @@
+import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
+import { readFile } from "fs/promises";
+import { join } from "path";
+import { spawn } from "child_process";
+
+interface GitHubRelease {
+ tag_name: string;
+ name: string;
+ published_at: string;
+ html_url: string;
+}
+
+export const versionRouter = createTRPCRouter({
+ // Get current local version
+ getCurrentVersion: publicProcedure
+ .query(async () => {
+ try {
+ const versionPath = join(process.cwd(), 'VERSION');
+ const version = await readFile(versionPath, 'utf-8');
+ return {
+ success: true,
+ version: version.trim()
+ };
+ } catch (error) {
+ console.error('Error reading VERSION file:', error);
+ return {
+ success: false,
+ error: 'Failed to read VERSION file',
+ version: null
+ };
+ }
+ }),
+
+ getLatestRelease: publicProcedure
+ .query(async () => {
+ try {
+ const response = await fetch('https://api.github.com/repos/community-scripts/ProxmoxVE-Local/releases/latest');
+
+ if (!response.ok) {
+ throw new Error(`GitHub API error: ${response.status}`);
+ }
+
+ const release: GitHubRelease = await response.json();
+
+ return {
+ success: true,
+ release: {
+ tagName: release.tag_name,
+ name: release.name,
+ publishedAt: release.published_at,
+ htmlUrl: release.html_url
+ }
+ };
+ } catch (error) {
+ console.error('Error fetching latest release:', error);
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : 'Failed to fetch latest release',
+ release: null
+ };
+ }
+ }),
+
+
+ getVersionStatus: publicProcedure
+ .query(async () => {
+ try {
+
+ const versionPath = join(process.cwd(), 'VERSION');
+ const currentVersion = (await readFile(versionPath, 'utf-8')).trim();
+
+
+ const response = await fetch('https://api.github.com/repos/community-scripts/ProxmoxVE-Local/releases/latest');
+
+ if (!response.ok) {
+ throw new Error(`GitHub API error: ${response.status}`);
+ }
+
+ const release: GitHubRelease = await response.json();
+ const latestVersion = release.tag_name.replace('v', '');
+
+
+ const isUpToDate = currentVersion === latestVersion;
+
+ return {
+ success: true,
+ currentVersion,
+ latestVersion,
+ isUpToDate,
+ updateAvailable: !isUpToDate,
+ releaseInfo: {
+ tagName: release.tag_name,
+ name: release.name,
+ publishedAt: release.published_at,
+ htmlUrl: release.html_url
+ }
+ };
+ } catch (error) {
+ console.error('Error checking version status:', error);
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : 'Failed to check version status',
+ currentVersion: null,
+ latestVersion: null,
+ isUpToDate: false,
+ updateAvailable: false,
+ releaseInfo: null
+ };
+ }
+ }),
+
+ // Execute update script
+ executeUpdate: publicProcedure
+ .mutation(async () => {
+ try {
+ const updateScriptPath = join(process.cwd(), 'update.sh');
+
+ // Spawn the update script as a detached process using nohup
+ // This allows it to run independently and kill the parent Node.js process
+ const child = spawn('nohup', ['bash', updateScriptPath], {
+ cwd: process.cwd(),
+ stdio: ['ignore', 'ignore', 'ignore'],
+ shell: false,
+ detached: true
+ });
+
+ // Unref the child process so it doesn't keep the parent alive
+ child.unref();
+
+ // Immediately return success since we can't wait for completion
+ // The script will handle its own logging and restart
+ return {
+ success: true,
+ message: 'Update started in background. The server will restart automatically when complete.',
+ output: '',
+ error: ''
+ };
+ } catch (error) {
+ console.error('Error executing update script:', error);
+ return {
+ success: false,
+ message: `Failed to execute update script: ${error instanceof Error ? error.message : 'Unknown error'}`,
+ output: '',
+ error: error instanceof Error ? error.message : 'Unknown error'
+ };
+ }
+ })
+});
diff --git a/update.sh b/update.sh
new file mode 100755
index 0000000..0020c3a
--- /dev/null
+++ b/update.sh
@@ -0,0 +1,895 @@
+#!/bin/bash
+
+# Enhanced update script for ProxmoxVE-Local
+# Fetches latest release from GitHub and backs up data directory
+
+set -euo pipefail # Exit on error, undefined vars, pipe failures
+
+# Add error trap for debugging
+trap 'echo "Error occurred at line $LINENO, command: $BASH_COMMAND"' ERR
+
+# Configuration
+REPO_OWNER="community-scripts"
+REPO_NAME="ProxmoxVE-Local"
+GITHUB_API="https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}"
+BACKUP_DIR="/tmp/pve-scripts-backup-$(date +%Y%m%d-%H%M%S)"
+DATA_DIR="./data"
+LOG_FILE="/tmp/update.log"
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# Initialize log file
+init_log() {
+ # Clear/create log file
+ > "$LOG_FILE"
+ log "Starting ProxmoxVE-Local update process..."
+ log "Log file: $LOG_FILE"
+}
+
+# Logging function
+log() {
+ echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1" | tee -a "$LOG_FILE" >&2
+}
+
+log_error() {
+ echo -e "${RED}[ERROR]${NC} $1" | tee -a "$LOG_FILE" >&2
+}
+
+log_success() {
+ echo -e "${GREEN}[SUCCESS]${NC} $1" | tee -a "$LOG_FILE" >&2
+}
+
+log_warning() {
+ echo -e "${YELLOW}[WARNING]${NC} $1" | tee -a "$LOG_FILE" >&2
+}
+
+# Check if required tools are available
+check_dependencies() {
+ log "Checking dependencies..."
+
+ local missing_deps=()
+
+ if ! command -v curl &> /dev/null; then
+ missing_deps+=("curl")
+ fi
+
+ if ! command -v jq &> /dev/null; then
+ missing_deps+=("jq")
+ fi
+
+ if ! command -v npm &> /dev/null; then
+ missing_deps+=("npm")
+ fi
+
+ if ! command -v node &> /dev/null; then
+ missing_deps+=("node")
+ fi
+
+ if [ ${#missing_deps[@]} -ne 0 ]; then
+ log_error "Missing dependencies: ${missing_deps[*]}"
+ log_error "Please install the missing dependencies and try again."
+ exit 1
+ fi
+
+ log_success "All dependencies are available"
+}
+
+# Get latest release info from GitHub API
+get_latest_release() {
+ log "Fetching latest release information from GitHub..."
+
+ local release_info
+ if ! release_info=$(curl -s --connect-timeout 15 --max-time 60 --retry 2 --retry-delay 3 "$GITHUB_API/releases/latest"); then
+ log_error "Failed to fetch release information from GitHub API (timeout or network error)"
+ exit 1
+ fi
+
+ # Check if response is valid JSON
+ if ! echo "$release_info" | jq empty 2>/dev/null; then
+ log_error "Invalid JSON response from GitHub API"
+ log "Response: $release_info"
+ exit 1
+ fi
+
+ local tag_name
+ local download_url
+ local published_at
+
+ tag_name=$(echo "$release_info" | jq -r '.tag_name')
+ download_url=$(echo "$release_info" | jq -r '.tarball_url')
+ published_at=$(echo "$release_info" | jq -r '.published_at')
+
+ if [ "$tag_name" = "null" ] || [ "$download_url" = "null" ] || [ -z "$tag_name" ] || [ -z "$download_url" ]; then
+ log_error "Failed to parse release information from API response"
+ log "Tag name: $tag_name"
+ log "Download URL: $download_url"
+ exit 1
+ fi
+
+ log_success "Latest release: $tag_name (published: $published_at)"
+ echo "$tag_name|$download_url"
+}
+
+# Backup data directory and .env file
+backup_data() {
+ log "Creating backup directory at $BACKUP_DIR..."
+
+ if ! mkdir -p "$BACKUP_DIR"; then
+ log_error "Failed to create backup directory"
+ exit 1
+ fi
+
+ # Backup data directory
+ if [ -d "$DATA_DIR" ]; then
+ log "Backing up data directory..."
+
+ if ! cp -r "$DATA_DIR" "$BACKUP_DIR/data"; then
+ log_error "Failed to backup data directory"
+ exit 1
+ else
+ log_success "Data directory backed up successfully"
+ fi
+ else
+ log_warning "Data directory not found, skipping backup"
+ fi
+
+ # Backup .env file
+ if [ -f ".env" ]; then
+ log "Backing up .env file..."
+ if ! cp ".env" "$BACKUP_DIR/.env"; then
+ log_error "Failed to backup .env file"
+ exit 1
+ else
+ log_success ".env file backed up successfully"
+ fi
+ else
+ log_warning ".env file not found, skipping backup"
+ fi
+}
+
+# Download and extract latest release
+download_release() {
+ local release_info="$1"
+ local tag_name="${release_info%|*}"
+ local download_url="${release_info#*|}"
+
+ log "Downloading release $tag_name..."
+
+ local temp_dir="/tmp/pve-update-$$"
+ local archive_file="$temp_dir/release.tar.gz"
+
+ # Create temporary directory
+ if ! mkdir -p "$temp_dir"; then
+ log_error "Failed to create temporary directory"
+ exit 1
+ fi
+
+ # Download release with timeout and progress
+ log "Downloading from: $download_url"
+ log "Target file: $archive_file"
+ log "Starting curl download..."
+
+ # Test if curl is working
+ log "Testing curl availability..."
+ if ! command -v curl >/dev/null 2>&1; then
+ log_error "curl command not found"
+ rm -rf "$temp_dir"
+ exit 1
+ fi
+
+ # Test basic connectivity
+ log "Testing basic connectivity..."
+ if ! curl -s --connect-timeout 10 --max-time 30 "https://api.github.com" >/dev/null 2>&1; then
+ log_error "Cannot reach GitHub API"
+ rm -rf "$temp_dir"
+ exit 1
+ fi
+ log_success "Connectivity test passed"
+
+ # Create a temporary file for curl output
+ local curl_log="/tmp/curl_log_$$.txt"
+
+ # Run curl with verbose output
+ if curl -L --connect-timeout 30 --max-time 300 --retry 3 --retry-delay 5 -v -o "$archive_file" "$download_url" > "$curl_log" 2>&1; then
+ log_success "Curl command completed successfully"
+ # Show some of the curl output for debugging
+ log "Curl output (first 10 lines):"
+ head -10 "$curl_log" | while read -r line; do
+ log "CURL: $line"
+ done
+ else
+ local curl_exit_code=$?
+ log_error "Curl command failed with exit code: $curl_exit_code"
+ log_error "Curl output:"
+ cat "$curl_log" | while read -r line; do
+ log_error "CURL: $line"
+ done
+ rm -f "$curl_log"
+ rm -rf "$temp_dir"
+ exit 1
+ fi
+
+ # Clean up curl log
+ rm -f "$curl_log"
+
+ # Verify download
+ if [ ! -f "$archive_file" ] || [ ! -s "$archive_file" ]; then
+ log_error "Downloaded file is empty or missing"
+ rm -rf "$temp_dir"
+ exit 1
+ fi
+
+ local file_size
+ file_size=$(stat -c%s "$archive_file" 2>/dev/null || echo "0")
+ log_success "Downloaded release ($file_size bytes)"
+
+ # Extract release
+ log "Extracting release..."
+ if ! tar -xzf "$archive_file" -C "$temp_dir"; then
+ log_error "Failed to extract release"
+ rm -rf "$temp_dir"
+ exit 1
+ fi
+
+ # Debug: List contents after extraction
+ log "Contents after extraction:"
+ ls -la "$temp_dir" >&2 || true
+
+ # Find the extracted directory (GitHub tarballs have a root directory)
+ log "Looking for extracted directory with pattern: ${REPO_NAME}-*"
+ local extracted_dir
+ extracted_dir=$(timeout 10 find "$temp_dir" -maxdepth 1 -type d -name "${REPO_NAME}-*" 2>/dev/null | head -1)
+
+ # If not found with repo name, try alternative patterns
+ if [ -z "$extracted_dir" ]; then
+ log "Trying pattern: community-scripts-ProxmoxVE-Local-*"
+ extracted_dir=$(timeout 10 find "$temp_dir" -maxdepth 1 -type d -name "community-scripts-ProxmoxVE-Local-*" 2>/dev/null | head -1)
+ fi
+
+ if [ -z "$extracted_dir" ]; then
+ log "Trying pattern: ProxmoxVE-Local-*"
+ extracted_dir=$(timeout 10 find "$temp_dir" -maxdepth 1 -type d -name "ProxmoxVE-Local-*" 2>/dev/null | head -1)
+ fi
+
+ if [ -z "$extracted_dir" ]; then
+ log "Trying any directory in temp folder"
+ extracted_dir=$(timeout 10 find "$temp_dir" -maxdepth 1 -type d ! -name "$temp_dir" 2>/dev/null | head -1)
+ fi
+
+ # If still not found, error out
+ if [ -z "$extracted_dir" ]; then
+ log_error "Could not find extracted directory"
+ rm -rf "$temp_dir"
+ exit 1
+ fi
+
+ log_success "Found extracted directory: $extracted_dir"
+ log_success "Release downloaded and extracted successfully"
+ echo "$extracted_dir"
+}
+
+# Clear the original directory before updating
+clear_original_directory() {
+ log "Clearing original directory..."
+
+ # List of files/directories to preserve (already backed up)
+ local preserve_patterns=(
+ "data"
+ ".env"
+ "*.log"
+ "update.log"
+ "*.backup"
+ "*.bak"
+ "node_modules"
+ ".git"
+ )
+
+ # Remove all files except preserved ones
+ while IFS= read -r file; do
+ local should_preserve=false
+ local filename=$(basename "$file")
+
+ for pattern in "${preserve_patterns[@]}"; do
+ if [[ "$filename" == $pattern ]]; then
+ should_preserve=true
+ break
+ fi
+ done
+
+ if [ "$should_preserve" = false ]; then
+ rm -f "$file"
+ fi
+ done < <(find . -maxdepth 1 -type f ! -name ".*")
+
+ # Remove all directories except preserved ones
+ while IFS= read -r dir; do
+ local should_preserve=false
+ local dirname=$(basename "$dir")
+
+ for pattern in "${preserve_patterns[@]}"; do
+ if [[ "$dirname" == $pattern ]]; then
+ should_preserve=true
+ break
+ fi
+ done
+
+ if [ "$should_preserve" = false ]; then
+ rm -rf "$dir"
+ fi
+ done < <(find . -maxdepth 1 -type d ! -name "." ! -name "..")
+
+ log_success "Original directory cleared"
+}
+
+# Restore backup files before building
+restore_backup_files() {
+ log "Restoring .env and data directory from backup..."
+
+ if [ -d "$BACKUP_DIR" ]; then
+ # Restore .env file
+ if [ -f "$BACKUP_DIR/.env" ]; then
+ if [ -f ".env" ]; then
+ rm -f ".env"
+ fi
+ if mv "$BACKUP_DIR/.env" ".env"; then
+ log_success ".env file restored from backup"
+ else
+ log_error "Failed to restore .env file"
+ return 1
+ fi
+ else
+ log_warning "No .env file backup found"
+ fi
+
+ # Restore data directory
+ if [ -d "$BACKUP_DIR/data" ]; then
+ if [ -d "data" ]; then
+ rm -rf "data"
+ fi
+ if mv "$BACKUP_DIR/data" "data"; then
+ log_success "Data directory restored from backup"
+ else
+ log_error "Failed to restore data directory"
+ return 1
+ fi
+ else
+ log_warning "No data directory backup found"
+ fi
+ else
+ log_error "No backup directory found for restoration"
+ return 1
+ fi
+}
+
+# Check if systemd service exists
+check_service() {
+ if systemctl list-unit-files | grep -q "^pvescriptslocal.service"; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+# Kill application processes directly
+kill_processes() {
+ # Try to find and stop the Node.js process
+ local pids
+ pids=$(pgrep -f "node server.js" 2>/dev/null || true)
+
+ # Also check for npm start processes
+ local npm_pids
+ npm_pids=$(pgrep -f "npm start" 2>/dev/null || true)
+
+ # Combine all PIDs
+ if [ -n "$npm_pids" ]; then
+ pids="$pids $npm_pids"
+ fi
+
+ if [ -n "$pids" ]; then
+ log "Stopping application processes: $pids"
+
+ # Send TERM signal to each PID individually
+ for pid in $pids; do
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
+ log "Sending TERM signal to PID: $pid"
+ kill -TERM "$pid" 2>/dev/null || true
+ fi
+ done
+
+ # Wait for graceful shutdown with timeout
+ log "Waiting for graceful shutdown..."
+ local wait_count=0
+ local max_wait=10 # Maximum 10 seconds
+
+ while [ $wait_count -lt $max_wait ]; do
+ local still_running
+ still_running=$(pgrep -f "node server.js\|npm start" 2>/dev/null || true)
+ if [ -z "$still_running" ]; then
+ log_success "Processes stopped gracefully"
+ break
+ fi
+ sleep 1
+ wait_count=$((wait_count + 1))
+ log "Waiting... ($wait_count/$max_wait)"
+ done
+
+ # Force kill any remaining processes
+ local remaining_pids
+ remaining_pids=$(pgrep -f "node server.js\|npm start" 2>/dev/null || true)
+ if [ -n "$remaining_pids" ]; then
+ log_warning "Force killing remaining processes: $remaining_pids"
+ pkill -9 -f "node server.js" 2>/dev/null || true
+ pkill -9 -f "npm start" 2>/dev/null || true
+ sleep 1
+ fi
+
+ # Final check
+ local final_check
+ final_check=$(pgrep -f "node server.js\|npm start" 2>/dev/null || true)
+ if [ -n "$final_check" ]; then
+ log_warning "Some processes may still be running: $final_check"
+ else
+ log_success "All application processes stopped"
+ fi
+ else
+ log "No running application processes found"
+ fi
+}
+
+# Kill application processes directly
+kill_processes() {
+ # Try to find and stop the Node.js process
+ local pids
+ pids=$(pgrep -f "node server.js" 2>/dev/null || true)
+
+ # Also check for npm start processes
+ local npm_pids
+ npm_pids=$(pgrep -f "npm start" 2>/dev/null || true)
+
+ # Combine all PIDs
+ if [ -n "$npm_pids" ]; then
+ pids="$pids $npm_pids"
+ fi
+
+ if [ -n "$pids" ]; then
+ log "Stopping application processes: $pids"
+
+ # Send TERM signal to each PID individually
+ for pid in $pids; do
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
+ log "Sending TERM signal to PID: $pid"
+ kill -TERM "$pid" 2>/dev/null || true
+ fi
+ done
+
+ # Wait for graceful shutdown with timeout
+ log "Waiting for graceful shutdown..."
+ local wait_count=0
+ local max_wait=10 # Maximum 10 seconds
+
+ while [ $wait_count -lt $max_wait ]; do
+ local still_running
+ still_running=$(pgrep -f "node server.js\|npm start" 2>/dev/null || true)
+ if [ -z "$still_running" ]; then
+ log_success "Processes stopped gracefully"
+ break
+ fi
+ sleep 1
+ wait_count=$((wait_count + 1))
+ log "Waiting... ($wait_count/$max_wait)"
+ done
+
+ # Force kill any remaining processes
+ local remaining_pids
+ remaining_pids=$(pgrep -f "node server.js\|npm start" 2>/dev/null || true)
+ if [ -n "$remaining_pids" ]; then
+ log_warning "Force killing remaining processes: $remaining_pids"
+ pkill -9 -f "node server.js" 2>/dev/null || true
+ pkill -9 -f "npm start" 2>/dev/null || true
+ sleep 1
+ fi
+
+ # Final check
+ local final_check
+ final_check=$(pgrep -f "node server.js\|npm start" 2>/dev/null || true)
+ if [ -n "$final_check" ]; then
+ log_warning "Some processes may still be running: $final_check"
+ else
+ log_success "All application processes stopped"
+ fi
+ else
+ log "No running application processes found"
+ fi
+}
+
+# Stop the application before updating
+stop_application() {
+ log "Stopping application..."
+
+ # Change to the application directory if we're not already there
+ local app_dir
+ if [ -f "package.json" ] && [ -f "server.js" ]; then
+ app_dir="$(pwd)"
+ else
+ # Try to find the application directory
+ app_dir=$(find /root -name "package.json" -path "*/ProxmoxVE-Local*" -exec dirname {} \; 2>/dev/null | head -1)
+ if [ -n "$app_dir" ] && [ -d "$app_dir" ]; then
+ cd "$app_dir" || {
+ log_error "Failed to change to application directory: $app_dir"
+ return 1
+ }
+ else
+ log_error "Could not find application directory"
+ return 1
+ fi
+ fi
+
+ log "Working from application directory: $(pwd)"
+
+ # Check if systemd service exists and is active
+ if check_service; then
+ if systemctl is-active --quiet pvescriptslocal.service; then
+ log "Stopping pvescriptslocal service..."
+ if systemctl stop pvescriptslocal.service; then
+ log_success "Service stopped successfully"
+ else
+ log_error "Failed to stop service, falling back to process kill"
+ kill_processes
+ fi
+ else
+ log "Service exists but is not active, checking for running processes..."
+ kill_processes
+ fi
+ else
+ log "No systemd service found, stopping processes directly..."
+ kill_processes
+ fi
+}
+
+# Update application files
+update_files() {
+ local source_dir="$1"
+
+ log "Updating application files..."
+
+ # List of files/directories to exclude from update
+ local exclude_patterns=(
+ "data"
+ "node_modules"
+ ".git"
+ ".env"
+ "*.log"
+ "update.log"
+ "*.backup"
+ "*.bak"
+ )
+
+ # Find the actual source directory (strip the top-level directory)
+ local actual_source_dir
+ actual_source_dir=$(find "$source_dir" -maxdepth 1 -type d -name "community-scripts-ProxmoxVE-Local-*" | head -1)
+
+ if [ -z "$actual_source_dir" ]; then
+ log_error "Could not find the actual source directory in $source_dir"
+ return 1
+ fi
+
+ # Use process substitution instead of pipe to avoid subshell issues
+ local files_copied=0
+ local files_excluded=0
+
+ log "Starting file copy process from: $actual_source_dir"
+
+ # Create a temporary file list to avoid process substitution issues
+ local file_list="/tmp/file_list_$$.txt"
+ find "$actual_source_dir" -type f > "$file_list"
+
+ local total_files
+ total_files=$(wc -l < "$file_list")
+ log "Found $total_files files to process"
+
+ # Show first few files for debugging
+ log "First few files to process:"
+ head -5 "$file_list" | while read -r f; do
+ log " - $f"
+ done
+
+ while IFS= read -r file; do
+ local rel_path="${file#$actual_source_dir/}"
+ local should_exclude=false
+
+ for pattern in "${exclude_patterns[@]}"; do
+ if [[ "$rel_path" == $pattern ]]; then
+ should_exclude=true
+ break
+ fi
+ done
+
+ if [ "$should_exclude" = false ]; then
+ local target_dir
+ target_dir=$(dirname "$rel_path")
+ if [ "$target_dir" != "." ]; then
+ mkdir -p "$target_dir"
+ fi
+ log "Copying: $file -> $rel_path"
+ if ! cp "$file" "$rel_path"; then
+ log_error "Failed to copy $rel_path"
+ rm -f "$file_list"
+ return 1
+ else
+ files_copied=$((files_copied + 1))
+ if [ $((files_copied % 10)) -eq 0 ]; then
+ log "Copied $files_copied files so far..."
+ fi
+ fi
+ else
+ files_excluded=$((files_excluded + 1))
+ log "Excluded: $rel_path"
+ fi
+ done < "$file_list"
+
+ # Clean up temporary file
+ rm -f "$file_list"
+
+ log "Files processed: $files_copied copied, $files_excluded excluded"
+
+ log_success "Application files updated successfully"
+}
+
+# Install dependencies and build
+install_and_build() {
+ log "Installing dependencies..."
+
+ if ! npm install; then
+ log_error "Failed to install dependencies"
+ return 1
+ fi
+
+ # Ensure no processes are running before build
+ log "Ensuring no conflicting processes are running..."
+ local pids
+ pids=$(pgrep -f "node server.js\|npm start" 2>/dev/null || true)
+ if [ -n "$pids" ]; then
+ log_warning "Found running processes, stopping them: $pids"
+ pkill -9 -f "node server.js" 2>/dev/null || true
+ pkill -9 -f "npm start" 2>/dev/null || true
+ sleep 2
+ fi
+
+ log "Building application..."
+ # Set NODE_ENV to production for build
+ export NODE_ENV=production
+
+ if ! npm run build; then
+ log_error "Failed to build application"
+ return 1
+ fi
+
+ log_success "Dependencies installed and application built successfully"
+}
+
+# Start the application after updating
+start_application() {
+ log "Starting application..."
+
+ # Check if systemd service exists
+ if check_service; then
+ log "Starting pvescriptslocal service..."
+ if systemctl start pvescriptslocal.service; then
+ log_success "Service started successfully"
+ # Wait a moment and check if it's running
+ sleep 2
+ if systemctl is-active --quiet pvescriptslocal.service; then
+ log_success "Service is running"
+ else
+ log_warning "Service started but may not be running properly"
+ fi
+ else
+ log_error "Failed to start service, falling back to npm start"
+ start_with_npm
+ fi
+ else
+ log "No systemd service found, starting with npm..."
+ start_with_npm
+ fi
+}
+
+# Start application with npm
+start_with_npm() {
+ log "Starting application with npm start..."
+
+ # Start in background
+ nohup npm start > server.log 2>&1 &
+ local npm_pid=$!
+
+ # Wait a moment and check if it started
+ sleep 3
+ if kill -0 $npm_pid 2>/dev/null; then
+ log_success "Application started with PID: $npm_pid"
+ else
+ log_error "Failed to start application with npm"
+ return 1
+ fi
+}
+
+# Rollback function
+rollback() {
+ log_warning "Rolling back to previous version..."
+
+ if [ -d "$BACKUP_DIR" ]; then
+ log "Restoring from backup directory: $BACKUP_DIR"
+
+ # Restore data directory
+ if [ -d "$BACKUP_DIR/data" ]; then
+ log "Restoring data directory..."
+ if [ -d "$DATA_DIR" ]; then
+ rm -rf "$DATA_DIR"
+ fi
+ if mv "$BACKUP_DIR/data" "$DATA_DIR"; then
+ log_success "Data directory restored from backup"
+ else
+ log_error "Failed to restore data directory"
+ fi
+ else
+ log_warning "No data directory backup found"
+ fi
+
+ # Restore .env file
+ if [ -f "$BACKUP_DIR/.env" ]; then
+ log "Restoring .env file..."
+ if [ -f ".env" ]; then
+ rm -f ".env"
+ fi
+ if mv "$BACKUP_DIR/.env" ".env"; then
+ log_success ".env file restored from backup"
+ else
+ log_error "Failed to restore .env file"
+ fi
+ else
+ log_warning "No .env file backup found"
+ fi
+
+ # Clean up backup directory
+ log "Cleaning up backup directory..."
+ rm -rf "$BACKUP_DIR"
+ else
+ log_error "No backup directory found for rollback"
+ fi
+
+ log_error "Update failed. Please check the logs and try again."
+ exit 1
+}
+
+# Main update process
+main() {
+ init_log
+
+ # Check if we're running from the application directory and not already relocated
+ if [ -z "${PVE_UPDATE_RELOCATED:-}" ] && [ -f "package.json" ] && [ -f "server.js" ]; then
+ log "Detected running from application directory"
+ log "Copying update script to temporary location for safe execution..."
+
+ local temp_script="/tmp/pve-scripts-update-$$.sh"
+ if ! cp "$0" "$temp_script"; then
+ log_error "Failed to copy update script to temporary location"
+ exit 1
+ fi
+
+ chmod +x "$temp_script"
+ log "Executing update from temporary location: $temp_script"
+
+ # Set flag to prevent infinite loop and execute from temporary location
+ export PVE_UPDATE_RELOCATED=1
+ exec "$temp_script" "$@"
+ fi
+
+ # Ensure we're in the application directory
+ local app_dir
+
+ # First check if we're already in the right directory
+ if [ -f "package.json" ] && [ -f "server.js" ]; then
+ app_dir="$(pwd)"
+ log "Already in application directory: $app_dir"
+ else
+ # Try multiple common locations
+ for search_path in /opt /root /home /usr/local; do
+ if [ -d "$search_path" ]; then
+ app_dir=$(find "$search_path" -name "package.json" -path "*/ProxmoxVE-Local*" -exec dirname {} \; 2>/dev/null | head -1)
+ if [ -n "$app_dir" ] && [ -d "$app_dir" ]; then
+ break
+ fi
+ fi
+ done
+
+ if [ -n "$app_dir" ] && [ -d "$app_dir" ]; then
+ cd "$app_dir" || {
+ log_error "Failed to change to application directory: $app_dir"
+ exit 1
+ }
+ log "Changed to application directory: $(pwd)"
+ else
+ log_error "Could not find application directory"
+ log "Searched in: /opt, /root, /home, /usr/local"
+ exit 1
+ fi
+ fi
+
+ # Check dependencies
+ check_dependencies
+
+ # Get latest release info
+ local release_info
+ release_info=$(get_latest_release)
+
+ # Backup data directory
+ backup_data
+
+ # Stop the application before updating (now running from /tmp/)
+ stop_application
+
+ # Double-check that no processes are running
+ local remaining_pids
+ remaining_pids=$(pgrep -f "node server.js\|npm start" 2>/dev/null || true)
+ if [ -n "$remaining_pids" ]; then
+ log_warning "Force killing remaining processes"
+ pkill -9 -f "node server.js" 2>/dev/null || true
+ pkill -9 -f "npm start" 2>/dev/null || true
+ sleep 2
+ fi
+
+ # Download and extract release
+ local source_dir
+ source_dir=$(download_release "$release_info")
+ log "Download completed, source_dir: $source_dir"
+
+ # Clear the original directory before updating
+ log "Clearing original directory..."
+ clear_original_directory
+ log "Original directory cleared successfully"
+
+ # Update files
+ log "Starting file update process..."
+ if ! update_files "$source_dir"; then
+ log_error "File update failed, rolling back..."
+ rollback
+ fi
+ log "File update completed successfully"
+
+ # Restore .env and data directory before building
+ log "Restoring backup files..."
+ restore_backup_files
+ log "Backup files restored successfully"
+
+ # Install dependencies and build
+ log "Starting install and build process..."
+ if ! install_and_build; then
+ log_error "Install and build failed, rolling back..."
+ rollback
+ fi
+ log "Install and build completed successfully"
+
+ # Cleanup
+ log "Cleaning up temporary files..."
+ rm -rf "$source_dir"
+ rm -rf "/tmp/pve-update-$$"
+
+ # Clean up temporary script if it exists
+ if [ -f "/tmp/pve-scripts-update-$$.sh" ]; then
+ rm -f "/tmp/pve-scripts-update-$$.sh"
+ fi
+
+ # Start the application
+ start_application
+
+ log_success "Update completed successfully!"
+}
+
+# Run main function with error handling
+if ! main "$@"; then
+ log_error "Update script failed with exit code $?"
+ exit 1
+fi
\ No newline at end of file