From 8d9f119e1c23a49db8ed9c9a4b86b641987fe359 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Tue, 14 Oct 2025 10:06:31 +0200 Subject: [PATCH 1/3] feat: Add Shell button for interactive LXC container access - Add Shell button to ScriptInstallationCard for SSH scripts with container_id - Implement shell state management in InstalledScriptsTab - Add shell execution methods in server.js (local and SSH) - Add isShell prop to Terminal component - Implement smooth scrolling to terminal when opened - Add highlight effect for better UX - Shell sessions are interactive (no auto-commands like update) The Shell button provides direct interactive access to LXC containers without automatically sending update commands, allowing users to manually execute commands in the container shell. --- server.js | 143 +++++++++++++++++- src/app/_components/InstalledScriptsTab.tsx | 110 +++++++++++++- .../_components/ScriptInstallationCard.tsx | 14 ++ src/app/_components/Terminal.tsx | 7 +- 4 files changed, 270 insertions(+), 4 deletions(-) diff --git a/server.js b/server.js index 4daf897c..2c75c495 100644 --- a/server.js +++ b/server.js @@ -207,13 +207,15 @@ class ScriptExecutionHandler { * @param {WebSocketMessage} message */ async handleMessage(ws, message) { - const { action, scriptPath, executionId, input, mode, server, isUpdate, containerId } = message; + const { action, scriptPath, executionId, input, mode, server, isUpdate, isShell, containerId } = message; switch (action) { case 'start': if (scriptPath && executionId) { if (isUpdate && containerId) { await this.startUpdateExecution(ws, containerId, executionId, mode, server); + } else if (isShell && containerId) { + await this.startShellExecution(ws, containerId, executionId, mode, server); } else { await this.startScriptExecution(ws, scriptPath, executionId, mode, server); } @@ -709,6 +711,145 @@ class ScriptExecutionHandler { }); } } + + /** + * Start shell execution + * @param {ExtendedWebSocket} ws + * @param {string} containerId + * @param {string} executionId + * @param {string} mode + * @param {ServerInfo} server + */ + async startShellExecution(ws, containerId, executionId, mode = 'local', server = null) { + try { + + // Send start message + this.sendMessage(ws, { + type: 'start', + data: `Starting shell session for container ${containerId}...`, + timestamp: Date.now() + }); + + if (mode === 'ssh' && server) { + await this.startSSHShellExecution(ws, containerId, executionId, server); + } else { + await this.startLocalShellExecution(ws, containerId, executionId); + } + + } catch (error) { + this.sendMessage(ws, { + type: 'error', + data: `Failed to start shell: ${error instanceof Error ? error.message : String(error)}`, + timestamp: Date.now() + }); + } + } + + /** + * Start local shell execution + * @param {ExtendedWebSocket} ws + * @param {string} containerId + * @param {string} executionId + */ + async startLocalShellExecution(ws, containerId, executionId) { + const { spawn } = await import('node-pty'); + + // Create a shell process that will run pct enter + const childProcess = spawn('bash', ['-c', `pct enter ${containerId}`], { + name: 'xterm-color', + cols: 80, + rows: 24, + cwd: process.cwd(), + env: process.env + }); + + // Store the execution + this.activeExecutions.set(executionId, { + process: childProcess, + ws + }); + + // Handle pty data + childProcess.onData((data) => { + this.sendMessage(ws, { + type: 'output', + data: data.toString(), + timestamp: Date.now() + }); + }); + + // Note: No automatic command is sent - user can type commands interactively + + // Handle process exit + childProcess.onExit((e) => { + this.sendMessage(ws, { + type: 'end', + data: `Shell session ended with exit code: ${e.exitCode}`, + timestamp: Date.now() + }); + + this.activeExecutions.delete(executionId); + }); + } + + /** + * Start SSH shell execution + * @param {ExtendedWebSocket} ws + * @param {string} containerId + * @param {string} executionId + * @param {ServerInfo} server + */ + async startSSHShellExecution(ws, containerId, executionId, server) { + const sshService = getSSHExecutionService(); + + try { + const execution = await sshService.executeCommand( + server, + `pct enter ${containerId}`, + /** @param {string} data */ + (data) => { + this.sendMessage(ws, { + type: 'output', + data: data, + timestamp: Date.now() + }); + }, + /** @param {string} error */ + (error) => { + this.sendMessage(ws, { + type: 'error', + data: error, + timestamp: Date.now() + }); + }, + /** @param {number} code */ + (code) => { + this.sendMessage(ws, { + type: 'end', + data: `Shell session ended with exit code: ${code}`, + timestamp: Date.now() + }); + + this.activeExecutions.delete(executionId); + } + ); + + // Store the execution + this.activeExecutions.set(executionId, { + process: /** @type {any} */ (execution).process, + ws + }); + + // Note: No automatic command is sent - user can type commands interactively + + } catch (error) { + this.sendMessage(ws, { + type: 'error', + data: `SSH shell execution failed: ${error instanceof Error ? error.message : String(error)}`, + timestamp: Date.now() + }); + } + } } // TerminalHandler removed - not used by current application diff --git a/src/app/_components/InstalledScriptsTab.tsx b/src/app/_components/InstalledScriptsTab.tsx index 6387e3d5..c528df0b 100644 --- a/src/app/_components/InstalledScriptsTab.tsx +++ b/src/app/_components/InstalledScriptsTab.tsx @@ -35,6 +35,7 @@ export function InstalledScriptsTab() { const [sortField, setSortField] = useState<'script_name' | 'container_id' | 'server_name' | 'status' | 'installation_date'>('server_name'); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); const [updatingScript, setUpdatingScript] = useState<{ id: number; containerId: string; server?: any } | null>(null); + const [openingShell, setOpeningShell] = useState<{ id: number; containerId: string; server?: any } | null>(null); const [editingScriptId, setEditingScriptId] = useState(null); const [editFormData, setEditFormData] = useState<{ script_name: string; container_id: string }>({ script_name: '', container_id: '' }); const [showAddForm, setShowAddForm] = useState(false); @@ -550,6 +551,87 @@ export function InstalledScriptsTab() { setUpdatingScript(null); }; + const handleOpenShell = (script: InstalledScript) => { + if (!script.container_id) { + setErrorModal({ + isOpen: true, + title: 'Shell Access Failed', + message: 'No Container ID available for this script', + details: 'This script does not have a valid container ID and cannot be accessed via shell.' + }); + return; + } + + // 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 + }; + } + + setOpeningShell({ + id: script.id, + containerId: script.container_id!, + server: server + }); + }; + + const handleCloseShellTerminal = () => { + setOpeningShell(null); + }; + + // Auto-scroll to terminals when they open + useEffect(() => { + if (openingShell) { + // Small delay to ensure the terminal is rendered + setTimeout(() => { + const terminalElement = document.querySelector('[data-terminal="shell"]'); + if (terminalElement) { + // Scroll to the terminal with smooth animation + terminalElement.scrollIntoView({ + behavior: 'smooth', + block: 'start', + inline: 'nearest' + }); + + // Add a subtle highlight effect + terminalElement.classList.add('animate-pulse'); + setTimeout(() => { + terminalElement.classList.remove('animate-pulse'); + }, 2000); + } + }, 200); + } + }, [openingShell]); + + useEffect(() => { + if (updatingScript) { + // Small delay to ensure the terminal is rendered + setTimeout(() => { + const terminalElement = document.querySelector('[data-terminal="update"]'); + if (terminalElement) { + // Scroll to the terminal with smooth animation + terminalElement.scrollIntoView({ + behavior: 'smooth', + block: 'start', + inline: 'nearest' + }); + + // Add a subtle highlight effect + terminalElement.classList.add('animate-pulse'); + setTimeout(() => { + terminalElement.classList.remove('animate-pulse'); + }, 2000); + } + }, 200); + } + }, [updatingScript]); + const handleEditScript = (script: InstalledScript) => { setEditingScriptId(script.id); setEditFormData({ @@ -662,7 +744,7 @@ export function InstalledScriptsTab() {
{/* Update Terminal */} {updatingScript && ( -
+
)} + {/* Shell Terminal */} + {openingShell && ( +
+ +
+ )} + {/* Header with Stats */}

Installed Scripts

@@ -995,6 +1091,7 @@ export function InstalledScriptsTab() { onSave={handleSaveEdit} onCancel={handleCancelEdit} onUpdate={() => handleUpdateScript(script)} + onShell={() => handleOpenShell(script)} onDelete={() => handleDeleteScript(Number(script.id))} isUpdating={updateScriptMutation.isPending} isDeleting={deleteScriptMutation.isPending} @@ -1203,6 +1300,17 @@ export function InstalledScriptsTab() { Update )} + {/* Shell button - only show for SSH scripts with container_id */} + {script.container_id && script.execution_mode === 'ssh' && ( + + )} {/* Container Control Buttons - only show for SSH scripts with container_id */} {script.container_id && script.execution_mode === 'ssh' && ( <> diff --git a/src/app/_components/ScriptInstallationCard.tsx b/src/app/_components/ScriptInstallationCard.tsx index 8c49e94e..a25cfe5a 100644 --- a/src/app/_components/ScriptInstallationCard.tsx +++ b/src/app/_components/ScriptInstallationCard.tsx @@ -31,6 +31,7 @@ interface ScriptInstallationCardProps { onSave: () => void; onCancel: () => void; onUpdate: () => void; + onShell: () => void; onDelete: () => void; isUpdating: boolean; isDeleting: boolean; @@ -50,6 +51,7 @@ export function ScriptInstallationCard({ onSave, onCancel, onUpdate, + onShell, onDelete, isUpdating, isDeleting, @@ -203,6 +205,18 @@ export function ScriptInstallationCard({ Update )} + {/* Shell button - only show for SSH scripts with container_id */} + {script.container_id && script.execution_mode === 'ssh' && ( + + )} {/* Container Control Buttons - only show for SSH scripts with container_id */} {script.container_id && script.execution_mode === 'ssh' && ( <> diff --git a/src/app/_components/Terminal.tsx b/src/app/_components/Terminal.tsx index db1d8550..b18e1084 100644 --- a/src/app/_components/Terminal.tsx +++ b/src/app/_components/Terminal.tsx @@ -11,6 +11,7 @@ interface TerminalProps { mode?: 'local' | 'ssh'; server?: any; isUpdate?: boolean; + isShell?: boolean; containerId?: string; } @@ -20,7 +21,7 @@ interface TerminalMessage { timestamp: number; } -export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate = false, containerId }: TerminalProps) { +export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate = false, isShell = false, containerId }: TerminalProps) { const [isConnected, setIsConnected] = useState(false); const [isRunning, setIsRunning] = useState(false); const [isClient, setIsClient] = useState(false); @@ -332,6 +333,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate mode, server, isUpdate, + isShell, containerId }; ws.send(JSON.stringify(message)); @@ -372,7 +374,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate wsRef.current.close(); } }; - }, [scriptPath, mode, server, isUpdate, containerId, isMobile]); // eslint-disable-line react-hooks/exhaustive-deps + }, [scriptPath, mode, server, isUpdate, isShell, containerId, isMobile]); // eslint-disable-line react-hooks/exhaustive-deps const startScript = () => { if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN && !isRunning) { @@ -388,6 +390,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate mode, server, isUpdate, + isShell, containerId })); } From 04a857599802578496f7f0594b4bf0ce9d952677 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Tue, 14 Oct 2025 10:10:49 +0200 Subject: [PATCH 2/3] fix: Include SSH authentication fields in installed scripts data - Add SSH key fields (auth_type, ssh_key, ssh_key_passphrase, ssh_port) to database query - Update InstalledScript interface to include SSH authentication fields - Fix server data construction in handleOpenShell and handleUpdateScript - Now properly supports SSH key authentication for shell and update operations This fixes the issue where SSH key authentication was not being used even when configured in server settings, as the installed scripts data was missing the SSH authentication fields. --- src/app/_components/InstalledScriptsTab.tsx | 20 +++++++++++++++---- .../_components/ScriptInstallationCard.tsx | 4 ++++ src/server/database.js | 4 ++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/app/_components/InstalledScriptsTab.tsx b/src/app/_components/InstalledScriptsTab.tsx index c528df0b..1b4fb53e 100644 --- a/src/app/_components/InstalledScriptsTab.tsx +++ b/src/app/_components/InstalledScriptsTab.tsx @@ -20,6 +20,10 @@ interface InstalledScript { server_ip: string | null; server_user: string | null; server_password: string | null; + server_auth_type: string | null; + server_ssh_key: string | null; + server_ssh_key_passphrase: string | null; + server_ssh_port: number | null; server_color: string | null; installation_date: string; status: 'in_progress' | 'success' | 'failed'; @@ -527,13 +531,17 @@ export function InstalledScriptsTab() { onConfirm: () => { // Get server info if it's SSH mode let server = null; - if (script.server_id && script.server_user && script.server_password) { + if (script.server_id && script.server_user) { server = { id: script.server_id, name: script.server_name, ip: script.server_ip, user: script.server_user, - password: script.server_password + password: script.server_password, + auth_type: script.server_auth_type || 'password', + ssh_key: script.server_ssh_key, + ssh_key_passphrase: script.server_ssh_key_passphrase, + ssh_port: script.server_ssh_port || 22 }; } @@ -564,13 +572,17 @@ export function InstalledScriptsTab() { // Get server info if it's SSH mode let server = null; - if (script.server_id && script.server_user && script.server_password) { + if (script.server_id && script.server_user) { server = { id: script.server_id, name: script.server_name, ip: script.server_ip, user: script.server_user, - password: script.server_password + password: script.server_password, + auth_type: script.server_auth_type || 'password', + ssh_key: script.server_ssh_key, + ssh_key_passphrase: script.server_ssh_key_passphrase, + ssh_port: script.server_ssh_port || 22 }; } diff --git a/src/app/_components/ScriptInstallationCard.tsx b/src/app/_components/ScriptInstallationCard.tsx index a25cfe5a..37350d51 100644 --- a/src/app/_components/ScriptInstallationCard.tsx +++ b/src/app/_components/ScriptInstallationCard.tsx @@ -14,6 +14,10 @@ interface InstalledScript { server_ip: string | null; server_user: string | null; server_password: string | null; + server_auth_type: string | null; + server_ssh_key: string | null; + server_ssh_key_passphrase: string | null; + server_ssh_port: number | null; server_color: string | null; installation_date: string; status: 'in_progress' | 'success' | 'failed'; diff --git a/src/server/database.js b/src/server/database.js index cf100d22..bca0659f 100644 --- a/src/server/database.js +++ b/src/server/database.js @@ -180,6 +180,10 @@ class DatabaseService { s.ip as server_ip, s.user as server_user, s.password as server_password, + s.auth_type as server_auth_type, + s.ssh_key as server_ssh_key, + s.ssh_key_passphrase as server_ssh_key_passphrase, + s.ssh_port as server_ssh_port, s.color as server_color FROM installed_scripts inst LEFT JOIN servers s ON inst.server_id = s.id From 2daa0cf71981610dcdf3ba34bf06334f5a6c2a2c Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Tue, 14 Oct 2025 10:19:09 +0200 Subject: [PATCH 3/3] fix: Resolve TypeScript and ESLint build errors - Replace logical OR (||) with nullish coalescing (??) operators - Remove unnecessary type assertion for container_id - Add missing dependencies to useEffect and useCallback hooks - Remove unused variable in SSHKeyInput component - Add isShell property to WebSocketMessage type definition - Fix ServerInfo type to allow null in shell execution methods All TypeScript and ESLint errors resolved, build now passes successfully. --- scripts/vm/openwrt-vm.sh | 651 -------------------- server.js | 3 +- src/app/_components/InstalledScriptsTab.tsx | 14 +- src/app/_components/SSHKeyInput.tsx | 3 - 4 files changed, 9 insertions(+), 662 deletions(-) delete mode 100644 scripts/vm/openwrt-vm.sh diff --git a/scripts/vm/openwrt-vm.sh b/scripts/vm/openwrt-vm.sh deleted file mode 100644 index da50011f..00000000 --- a/scripts/vm/openwrt-vm.sh +++ /dev/null @@ -1,651 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) 2021-2025 tteck -# Author: tteck (tteckster) -# Jon Spriggs (jontheniceguy) -# License: MIT -# https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE -# Based on work from https://i12bretro.github.io/tutorials/0405.html - -source /dev/stdin <<<$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func) - -function header_info { - clear - cat <<"EOF" - ____ _ __ __ - / __ \____ ___ ____| | / /____/ /_ - / / / / __ \/ _ \/ __ \ | /| / / ___/ __/ -/ /_/ / /_/ / __/ / / / |/ |/ / / / /_ -\____/ .___/\___/_/ /_/|__/|__/_/ \__/ - /_/ W I R E L E S S F R E E D O M - -EOF -} -header_info -echo -e "\n Loading..." -RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" -METHOD="" -NSAPP="openwrt-vm" -var_os="openwrt" -var_version=" " -DISK_SIZE="1G" -GEN_MAC=02:$(openssl rand -hex 5 | awk '{print toupper($0)}' | sed 's/\(..\)/\1:/g; s/.$//') -GEN_MAC_LAN=02:$(openssl rand -hex 5 | awk '{print toupper($0)}' | sed 's/\(..\)/\1:/g; s/.$//') - -YW=$(echo "\033[33m") -BL=$(echo "\033[36m") -HA=$(echo "\033[1;34m") -RD=$(echo "\033[01;31m") -BGN=$(echo "\033[4;92m") -GN=$(echo "\033[1;92m") -DGN=$(echo "\033[32m") -CL=$(echo "\033[m") - -BOLD=$(echo "\033[1m") -BFR="\\r\\033[K" -HOLD=" " -TAB=" " - -CM="${TAB}✔️${TAB}${CL}" -CROSS="${TAB}✖️${TAB}${CL}" -INFO="${TAB}💡${TAB}${CL}" -OS="${TAB}🖥️${TAB}${CL}" -CONTAINERTYPE="${TAB}📦${TAB}${CL}" -DISKSIZE="${TAB}💾${TAB}${CL}" -CPUCORE="${TAB}🧠${TAB}${CL}" -RAMSIZE="${TAB}🛠️${TAB}${CL}" -CONTAINERID="${TAB}🆔${TAB}${CL}" -HOSTNAME="${TAB}🏠${TAB}${CL}" -BRIDGE="${TAB}🌉${TAB}${CL}" -GATEWAY="${TAB}🌐${TAB}${CL}" -DEFAULT="${TAB}⚙️${TAB}${CL}" -MACADDRESS="${TAB}🔗${TAB}${CL}" -VLANTAG="${TAB}🏷️${TAB}${CL}" -CREATING="${TAB}🚀${TAB}${CL}" -ADVANCED="${TAB}🧩${TAB}${CL}" -CLOUD="${TAB}☁️${TAB}${CL}" - -set -Eeo pipefail -trap 'error_handler $LINENO "$BASH_COMMAND"' ERR -trap cleanup EXIT -trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT -trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM -function error_handler() { - local exit_code="$?" - local line_number="$1" - local command="$2" - post_update_to_api "failed" "$command" - local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" - echo -e "\n$error_message\n" - cleanup_vmid -} - -function get_valid_nextid() { - local try_id - try_id=$(pvesh get /cluster/nextid) - while true; do - if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then - try_id=$((try_id + 1)) - continue - fi - if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then - try_id=$((try_id + 1)) - continue - fi - break - done - echo "$try_id" -} - -function cleanup_vmid() { - if qm status $VMID &>/dev/null; then - qm stop $VMID &>/dev/null - qm destroy $VMID &>/dev/null - fi -} - -function cleanup() { - popd >/dev/null - rm -rf $TEMP_DIR -} - -TEMP_DIR=$(mktemp -d) -pushd $TEMP_DIR >/dev/null -function send_line_to_vm() { - echo -e "${DGN}Sending line: ${YW}$1${CL}" - for ((i = 0; i < ${#1}; i++)); do - character=${1:i:1} - case $character in - " ") character="spc" ;; - "-") character="minus" ;; - "=") character="equal" ;; - ",") character="comma" ;; - ".") character="dot" ;; - "/") character="slash" ;; - "'") character="apostrophe" ;; - ";") character="semicolon" ;; - '\') character="backslash" ;; - '`') character="grave_accent" ;; - "[") character="bracket_left" ;; - "]") character="bracket_right" ;; - "_") character="shift-minus" ;; - "+") character="shift-equal" ;; - "?") character="shift-slash" ;; - "<") character="shift-comma" ;; - ">") character="shift-dot" ;; - '"') character="shift-apostrophe" ;; - ":") character="shift-semicolon" ;; - "|") character="shift-backslash" ;; - "~") character="shift-grave_accent" ;; - "{") character="shift-bracket_left" ;; - "}") character="shift-bracket_right" ;; - "A") character="shift-a" ;; - "B") character="shift-b" ;; - "C") character="shift-c" ;; - "D") character="shift-d" ;; - "E") character="shift-e" ;; - "F") character="shift-f" ;; - "G") character="shift-g" ;; - "H") character="shift-h" ;; - "I") character="shift-i" ;; - "J") character="shift-j" ;; - "K") character="shift-k" ;; - "L") character="shift-l" ;; - "M") character="shift-m" ;; - "N") character="shift-n" ;; - "O") character="shift-o" ;; - "P") character="shift-p" ;; - "Q") character="shift-q" ;; - "R") character="shift-r" ;; - "S") character="shift-s" ;; - "T") character="shift-t" ;; - "U") character="shift-u" ;; - "V") character="shift-v" ;; - "W") character="shift-w" ;; - "X") character="shift=x" ;; - "Y") character="shift-y" ;; - "Z") character="shift-z" ;; - "!") character="shift-1" ;; - "@") character="shift-2" ;; - "#") character="shift-3" ;; - '$') character="shift-4" ;; - "%") character="shift-5" ;; - "^") character="shift-6" ;; - "&") character="shift-7" ;; - "*") character="shift-8" ;; - "(") character="shift-9" ;; - ")") character="shift-0" ;; - esac - qm sendkey $VMID "$character" - done - qm sendkey $VMID ret -} - -TEMP_DIR=$(mktemp -d) -pushd $TEMP_DIR >/dev/null - -if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "OpenWrt VM" --yesno "This will create a New OpenWrt VM. Proceed?" 10 58); then - : -else - header_info && echo -e "⚠ User exited script \n" && exit -fi - -function msg_info() { - local msg="$1" - echo -ne " ${HOLD} ${YW}${msg}..." -} - -function msg_ok() { - local msg="$1" - echo -e "${BFR} ${CM} ${GN}${msg}${CL}" -} - -function msg_error() { - local msg="$1" - echo -e "${BFR} ${CROSS} ${RD}${msg}${CL}" -} - -# This function checks the version of Proxmox Virtual Environment (PVE) and exits if the version is not supported. -# Supported: Proxmox VE 8.0.x – 8.9.x and 9.0 (NOT 9.1+) -pve_check() { - local PVE_VER - PVE_VER="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')" - - # Check for Proxmox VE 8.x: allow 8.0–8.9 - if [[ "$PVE_VER" =~ ^8\.([0-9]+) ]]; then - local MINOR="${BASH_REMATCH[1]}" - if ((MINOR < 0 || MINOR > 9)); then - msg_error "This version of Proxmox VE is not supported." - msg_error "Supported: Proxmox VE version 8.0 – 8.9" - exit 1 - fi - return 0 - fi - - # Check for Proxmox VE 9.x: allow ONLY 9.0 - if [[ "$PVE_VER" =~ ^9\.([0-9]+) ]]; then - local MINOR="${BASH_REMATCH[1]}" - if ((MINOR != 0)); then - msg_error "This version of Proxmox VE is not yet supported." - msg_error "Supported: Proxmox VE version 9.0" - exit 1 - fi - return 0 - fi - - # All other unsupported versions - msg_error "This version of Proxmox VE is not supported." - msg_error "Supported versions: Proxmox VE 8.0 – 8.x or 9.0" - exit 1 -} - -function arch_check() { - if [ "$(dpkg --print-architecture)" != "amd64" ]; then - echo -e "\n ${CROSS} This script will not work with PiMox! \n" - echo -e "Exiting..." - sleep 2 - exit - fi -} - -function ssh_check() { - if command -v pveversion >/dev/null 2>&1; then - if [ -n "${SSH_CLIENT:+x}" ]; then - if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then - echo "you've been warned" - else - clear - exit - fi - fi - fi -} - -function exit-script() { - clear - echo -e "⚠ User exited script \n" - exit -} - -function default_settings() { - VMID=$(get_valid_nextid) - HN="openwrt" - CORE_COUNT="1" - RAM_SIZE="256" - BRG="vmbr0" - LAN_BRG="vmbr0" - MAC=$GEN_MAC - LAN_MAC=$GEN_MAC_LAN - VLAN="" - LAN_VLAN=",tag=999" - LAN_IP_ADDR="192.168.1.1" - LAN_NETMASK="255.255.255.0" - MTU="" - START_VM="yes" - METHOD="default" - DISK_SIZE="1G" - echo -e "${CONTAINERID}${BOLD}${DGN}VMID: ${BGN}${VMID}${CL}" - echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}" - echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}" - echo -e "${RAMSIZE}${BOLD}${DGN}RAM: ${BGN}${RAM_SIZE}${CL}" - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE}${CL}" - echo -e "${BRIDGE}${BOLD}${DGN}WAN Bridge: ${BGN}${BRG}${CL}" - echo -e "${BRIDGE}${BOLD}${DGN}LAN Bridge: ${BGN}${LAN_BRG}${CL}" - echo -e "${MACADDRESS}${BOLD}${DGN}WAN MAC: ${BGN}${MAC}${CL}" - echo -e "${MACADDRESS}${BOLD}${DGN}LAN MAC: ${BGN}${LAN_MAC}${CL}" -} - -function advanced_settings() { - METHOD="advanced" - [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid) - while true; do - if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z "$VMID" ]; then - VMID=$(get_valid_nextid) - fi - if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then - echo -e "${CROSS}${RD} ID $VMID is already in use${CL}" - sleep 2 - continue - fi - echo -e "${DGN}Virtual Machine ID: ${BGN}$VMID${CL}" - break - else - exit-script - fi - done - - if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 openwrt --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $VM_NAME ]; then - HN="openwrt" - else - HN=$(echo ${VM_NAME,,} | tr -d ' ') - fi - echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}" - else - exit-script - fi - - if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 1 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $CORE_COUNT ]; then - CORE_COUNT="1" - fi - echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}" - else - exit-script - fi - - if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 256 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $RAM_SIZE ]; then - RAM_SIZE="256" - fi - echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}" - else - exit-script - fi - - if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --inputbox "Set Disk Size in GiB (e.g., 1, 2, 4)" 8 58 "1" \ - --title "DISK SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [[ "$DISK_SIZE" =~ ^[0-9]+$ ]]; then - DISK_SIZE="${DISK_SIZE}G" - fi - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE}${CL}" - else - exit-script - fi - - if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a WAN Bridge" 8 58 vmbr0 --title "WAN BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $BRG ]; then - BRG="vmbr0" - fi - echo -e "${DGN}Using WAN Bridge: ${BGN}$BRG${CL}" - else - exit-script - fi - - if LAN_BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN Bridge" 8 58 vmbr0 --title "LAN BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $LAN_BRG ]; then - LAN_BRG="vmbr0" - fi - echo -e "${DGN}Using LAN Bridge: ${BGN}$LAN_BRG${CL}" - else - exit-script - fi - - if LAN_IP_ADDR=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a router IP" 8 58 $LAN_IP_ADDR --title "LAN IP ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $LAN_IP_ADDR ]; then - LAN_IP_ADDR="192.168.1.1" - fi - echo -e "${DGN}Using LAN IP ADDRESS: ${BGN}$LAN_IP_ADDR${CL}" - else - exit-script - fi - - if LAN_NETMASK=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a router netmask" 8 58 $LAN_NETMASK --title "LAN NETMASK" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $LAN_NETMASK ]; then - LAN_NETMASK="255.255.255.0" - fi - echo -e "${DGN}Using LAN NETMASK: ${BGN}$LAN_NETMASK${CL}" - else - exit-script - fi - - if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a WAN MAC Address" 8 58 $GEN_MAC --title "WAN MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $MAC1 ]; then - MAC="$GEN_MAC" - else - MAC="$MAC1" - fi - echo -e "${DGN}Using WAN MAC Address: ${BGN}$MAC${CL}" - else - exit-script - fi - - if MAC2=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN MAC Address" 8 58 $GEN_MAC_LAN --title "LAN MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $MAC2 ]; then - LAN_MAC="$GEN_MAC_LAN" - else - LAN_MAC="$MAC2" - fi - echo -e "${DGN}Using LAN MAC Address: ${BGN}$LAN_MAC${CL}" - else - exit-script - fi - - if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a WAN Vlan (leave blank for default)" 8 58 --title "WAN VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $VLAN1 ]; then - VLAN1="Default" - VLAN="" - else - VLAN=",tag=$VLAN1" - fi - echo -e "${DGN}Using WAN Vlan: ${BGN}$VLAN1${CL}" - else - exit-script - fi - - if VLAN2=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN Vlan" 8 58 999 --title "LAN VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $VLAN2 ]; then - VLAN2="999" - LAN_VLAN=",tag=$VLAN2" - else - LAN_VLAN=",tag=$VLAN2" - fi - echo -e "${DGN}Using LAN Vlan: ${BGN}$VLAN2${CL}" - else - exit-script - fi - - if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $MTU1 ]; then - MTU1="Default" - MTU="" - else - MTU=",mtu=$MTU1" - fi - echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}" - else - exit-script - fi - - if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then - START_VM="yes" - else - START_VM="no" - fi - echo -e "${DGN}Start VM when completed: ${BGN}$START_VM${CL}" - - if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create OpenWrt VM?" --no-button Do-Over 10 58); then - echo -e "${RD}Creating a OpenWrt VM using the above advanced settings${CL}" - else - header_info - echo -e "${RD}Using Advanced Settings${CL}" - advanced_settings - fi -} - -function start_script() { - if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then - header_info - echo -e "${BL}Using Default Settings${CL}" - default_settings - else - header_info - echo -e "${RD}Using Advanced Settings${CL}" - advanced_settings - fi -} - -arch_check -pve_check -ssh_check -start_script -post_to_api_vm - -msg_info "Validating Storage" -while read -r line; do - TAG=$(echo $line | awk '{print $1}') - TYPE=$(echo $line | awk '{printf "%-10s", $2}') - FREE=$(echo $line | numfmt --field 4-6 --from-unit=K --to=iec --format %.2f | awk '{printf( "%9sB", $6)}') - ITEM=" Type: $TYPE Free: $FREE " - OFFSET=2 - if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then - MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET)) - fi - STORAGE_MENU+=("$TAG" "$ITEM" "OFF") -done < <(pvesm status -content images | awk 'NR>1') -VALID=$(pvesm status -content images | awk 'NR>1') -if [ -z "$VALID" ]; then - echo -e "\n${RD}⚠ Unable to detect a valid storage location.${CL}" - echo -e "Exiting..." - exit -elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then - STORAGE=${STORAGE_MENU[0]} -else - while [ -z "${STORAGE:+x}" ]; do - STORAGE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Storage Pools" --radiolist \ - "Which storage pool would you like to use for the OpenWrt VM?\n\n" \ - 16 $(($MSG_MAX_LENGTH + 23)) 6 \ - "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) - done -fi -msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location." -msg_ok "Virtual Machine ID is ${CL}${BL}$VMID${CL}." -msg_info "Getting URL for OpenWrt Disk Image" - -response=$(curl -fsSL https://openwrt.org) -stableversion=$(echo "$response" | sed -n 's/.*Current stable release - OpenWrt \([0-9.]\+\).*/\1/p' | head -n 1) -URL="https://downloads.openwrt.org/releases/$stableversion/targets/x86/64/openwrt-$stableversion-x86-64-generic-ext4-combined.img.gz" - -msg_ok "${CL}${BL}${URL}${CL}" -curl -f#SL -o "$(basename "$URL")" "$URL" -FILE=$(basename "$URL") -msg_ok "Downloaded ${CL}${BL}$FILE${CL}" - -gunzip -f "$FILE" >/dev/null 2>&1 || true -FILE="${FILE%.*}" -msg_ok "Extracted OpenWrt Disk Image ${CL}${BL}$FILE${CL}" - -msg_info "Creating OpenWrt VM" -qm create "$VMID" -cores "$CORE_COUNT" -memory "$RAM_SIZE" -name "$HN" \ - -onboot 1 -ostype l26 -scsihw virtio-scsi-pci --tablet 0 -if [[ "$(pvesm status | awk -v s=$STORAGE '$1==s {print $2}')" == "dir" ]]; then - qm set "$VMID" -efidisk0 "${STORAGE}:0,efitype=4m,size=4M" -else - pvesm alloc "$STORAGE" "$VMID" "vm-$VMID-disk-0" 4M >/dev/null - qm set "$VMID" -efidisk0 "${STORAGE}:vm-$VMID-disk-0,efitype=4m,size=4M" -fi - -IMPORT_OUT="$(qm importdisk "$VMID" "$FILE" "$STORAGE" --format raw 2>&1 || true)" -DISK_REF="$(printf '%s\n' "$IMPORT_OUT" | sed -n "s/.*successfully imported disk '\([^']\+\)'.*/\1/p")" - -if [[ -z "$DISK_REF" ]]; then - DISK_REF="$(pvesm list "$STORAGE" | awk -v id="$VMID" '$1 ~ ("vm-"id"-disk-") {print $1}' | sort | tail -n1)" -fi - -if [[ -z "$DISK_REF" ]]; then - msg_error "Unable to determine imported disk reference." - echo "$IMPORT_OUT" - exit 1 -fi - -qm set "$VMID" \ - -efidisk0 "${STORAGE}:0,efitype=4m,size=4M" \ - -scsi0 "${DISK_REF},size=${DISK_SIZE}" \ - -boot order=scsi0 \ - -tags community-script >/dev/null -msg_ok "Attached disk (${DISK_SIZE})" - -DESCRIPTION=$( - cat < - - Logo - - -

OpenWrt VM

- -

- - spend Coffee - -

- - - - GitHub - - - - Discussions - - - - Issues - -
-EOF -) -qm set "$VMID" -description "$DESCRIPTION" >/dev/null - -msg_ok "Created OpenWrt VM ${CL}${BL}(${HN})" -msg_info "OpenWrt is being started in order to configure the network interfaces." -qm start $VMID -sleep 15 -msg_info "Waiting for OpenWrt to boot..." -for i in {1..30}; do - if qm status "$VMID" | grep -q "running"; then - sleep 5 - msg_ok "OpenWrt is running" - break - fi - sleep 1 -done - -msg_ok "Network interfaces are being configured as OpenWrt initiates." - -if qm status "$VMID" | grep -q "running"; then - send_line_to_vm "" - send_line_to_vm "uci delete network.@device[0]" - send_line_to_vm "uci set network.wan=interface" - send_line_to_vm "uci set network.wan.device=eth1" - send_line_to_vm "uci set network.wan.proto=dhcp" - send_line_to_vm "uci delete network.lan" - send_line_to_vm "uci set network.lan=interface" - send_line_to_vm "uci set network.lan.device=eth0" - send_line_to_vm "uci set network.lan.proto=static" - send_line_to_vm "uci set network.lan.ipaddr=${LAN_IP_ADDR}" - send_line_to_vm "uci set network.lan.netmask=${LAN_NETMASK}" - send_line_to_vm "uci commit" - send_line_to_vm "halt" - msg_ok "Network interfaces configured in OpenWrt" -else - msg_error "VM is not running" - exit 1 -fi - -msg_info "Waiting for OpenWrt to shut down..." -until qm status "$VMID" | grep -q "stopped"; do - sleep 2 -done -msg_ok "OpenWrt has shut down" - -msg_info "Adding bridge interfaces on Proxmox side" -qm set "$VMID" \ - -net0 virtio,bridge="${LAN_BRG}",macaddr="${LAN_MAC}${LAN_VLAN}${MTU}" \ - -net1 virtio,bridge="${BRG}",macaddr="${MAC}${VLAN}${MTU}" >/dev/null -msg_ok "Bridge interfaces added" - -if [ "$START_VM" = "yes" ]; then - msg_info "Starting OpenWrt VM" - qm start "$VMID" - msg_ok "Started OpenWrt VM" -fi - -VLAN_FINISH="" -if [ -z "$VLAN" ] && [ "$VLAN2" != "999" ]; then - VLAN_FINISH=" Please remember to adjust the VLAN tags to suit your network." -fi -post_update_to_api "done" "none" -msg_ok "Completed Successfully!${VLAN_FINISH:+\n$VLAN_FINISH}" diff --git a/server.js b/server.js index 2c75c495..ab89406c 100644 --- a/server.js +++ b/server.js @@ -51,6 +51,7 @@ const handle = app.getRequestHandler(); * @property {string} [mode] * @property {ServerInfo} [server] * @property {boolean} [isUpdate] + * @property {boolean} [isShell] * @property {string} [containerId] */ @@ -718,7 +719,7 @@ class ScriptExecutionHandler { * @param {string} containerId * @param {string} executionId * @param {string} mode - * @param {ServerInfo} server + * @param {ServerInfo|null} server */ async startShellExecution(ws, containerId, executionId, mode = 'local', server = null) { try { diff --git a/src/app/_components/InstalledScriptsTab.tsx b/src/app/_components/InstalledScriptsTab.tsx index 1b4fb53e..12f1477d 100644 --- a/src/app/_components/InstalledScriptsTab.tsx +++ b/src/app/_components/InstalledScriptsTab.tsx @@ -345,7 +345,7 @@ export function InstalledScriptsTab() { containerStatusMutation.mutate({ serverIds }); } }, 500); - }, []); // Remove containerStatusMutation from dependencies to prevent loops + }, [containerStatusMutation]); // Run cleanup when component mounts and scripts are loaded (only once) useEffect(() => { @@ -361,7 +361,7 @@ export function InstalledScriptsTab() { console.log('Status check triggered - scripts length:', scripts.length); fetchContainerStatuses(); } - }, [scripts.length]); // Remove fetchContainerStatuses from dependencies + }, [scripts.length, fetchContainerStatuses]); // Cleanup timeout on unmount useEffect(() => { @@ -538,10 +538,10 @@ export function InstalledScriptsTab() { ip: script.server_ip, user: script.server_user, password: script.server_password, - auth_type: script.server_auth_type || 'password', + auth_type: script.server_auth_type ?? 'password', ssh_key: script.server_ssh_key, ssh_key_passphrase: script.server_ssh_key_passphrase, - ssh_port: script.server_ssh_port || 22 + ssh_port: script.server_ssh_port ?? 22 }; } @@ -579,16 +579,16 @@ export function InstalledScriptsTab() { ip: script.server_ip, user: script.server_user, password: script.server_password, - auth_type: script.server_auth_type || 'password', + auth_type: script.server_auth_type ?? 'password', ssh_key: script.server_ssh_key, ssh_key_passphrase: script.server_ssh_key_passphrase, - ssh_port: script.server_ssh_port || 22 + ssh_port: script.server_ssh_port ?? 22 }; } setOpeningShell({ id: script.id, - containerId: script.container_id!, + containerId: script.container_id, server: server }); }; diff --git a/src/app/_components/SSHKeyInput.tsx b/src/app/_components/SSHKeyInput.tsx index bd2f74a2..93bd5957 100644 --- a/src/app/_components/SSHKeyInput.tsx +++ b/src/app/_components/SSHKeyInput.tsx @@ -104,9 +104,6 @@ export function SSHKeyInput({ value, onChange, onError, disabled = false }: SSHK keyType = 'ECDSA'; } else if (keyLine.includes('OPENSSH PRIVATE KEY')) { // For OpenSSH format keys, try to detect type from the key content - // Look for common patterns in the base64 content - const base64Content = keyContent.replace(/-----BEGIN.*?-----/, '').replace(/-----END.*?-----/, '').replace(/\s/g, ''); - // This is a heuristic - OpenSSH ED25519 keys typically start with specific patterns // We'll default to "OpenSSH" for now since we can't reliably detect the type keyType = 'OpenSSH';