Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot]
dcb5402c74 chore: add VERSION v0.5.2 2025-12-05 15:13:37 +00:00
25 changed files with 1788 additions and 4187 deletions

View File

@@ -4,7 +4,7 @@
## 🔗 Related PR / Issue
Fixes: #
Link: #
## ✅ Prerequisites (**X** in brackets)

View File

@@ -100,7 +100,7 @@ apt install -y nodejs
```bash
# Clone the repository
git clone https://github.com/community-scripts/ProxmoxVE-Local.git /opt/PVESciptslocal
cd /opt/PVESciptslocal
cd PVESciptslocal
# Install dependencies and build
npm install

View File

@@ -1 +1 @@
0.5.5
0.5.2

1180
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -25,35 +25,35 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@prisma/adapter-better-sqlite3": "^7.2.0",
"@prisma/client": "^7.2.0",
"@prisma/adapter-better-sqlite3": "^7.1.0",
"@prisma/client": "^7.1.0",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-slot": "^1.2.4",
"@t3-oss/env-nextjs": "^0.13.10",
"@t3-oss/env-nextjs": "^0.13.8",
"@tailwindcss/typography": "^0.5.19",
"@tanstack/react-query": "^5.90.18",
"@trpc/client": "^11.8.1",
"@trpc/react-query": "^11.8.1",
"@trpc/server": "^11.8.1",
"@tanstack/react-query": "^5.90.11",
"@trpc/client": "^11.7.2",
"@trpc/react-query": "^11.7.2",
"@trpc/server": "^11.7.2",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/ws": "^8.18.1",
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
"axios": "^1.13.2",
"bcryptjs": "^3.0.3",
"better-sqlite3": "^12.6.0",
"better-sqlite3": "^12.5.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cron-validator": "^1.4.0",
"dotenv": "^17.2.3",
"jsonwebtoken": "^9.0.3",
"lucide-react": "^0.562.0",
"next": "^16.1.3",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.555.0",
"next": "^16.0.7",
"node-cron": "^4.2.1",
"node-pty": "^1.1.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"node-pty": "^1.0.0",
"react": "^19.2.0",
"react-dom": "^19.2.1",
"react-markdown": "^10.1.0",
"react-syntax-highlighter": "^16.1.0",
"refractor": "^5.0.0",
@@ -62,37 +62,37 @@
"strip-ansi": "^7.1.2",
"superjson": "^2.2.6",
"tailwind-merge": "^3.4.0",
"ws": "^8.19.0",
"zod": "^4.3.5"
"ws": "^8.18.3",
"zod": "^4.1.13"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.18",
"@tailwindcss/postcss": "^4.1.17",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/bcryptjs": "^3.0.0",
"@types/better-sqlite3": "^7.6.13",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^24.10.9",
"@types/node": "^24.10.1",
"@types/node-cron": "^3.0.11",
"@types/react": "^19.2.8",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2",
"@vitest/coverage-v8": "^4.0.17",
"@vitest/ui": "^4.0.17",
"baseline-browser-mapping": "^2.9.15",
"eslint": "^9.39.2",
"eslint-config-next": "^16.1.3",
"jsdom": "^27.4.0",
"@vitejs/plugin-react": "^5.1.1",
"@vitest/coverage-v8": "^4.0.15",
"@vitest/ui": "^4.0.14",
"baseline-browser-mapping": "^2.8.32",
"eslint": "^9.39.1",
"eslint-config-next": "^16.0.6",
"jsdom": "^27.2.0",
"postcss": "^8.5.6",
"prettier": "^3.8.0",
"prettier": "^3.7.3",
"prettier-plugin-tailwindcss": "^0.7.2",
"prisma": "^7.2.0",
"tailwindcss": "^4.1.18",
"prisma": "^7.1.0",
"tailwindcss": "^4.1.17",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.53.0",
"vitest": "^4.0.17"
"typescript-eslint": "^8.48.1",
"vitest": "^4.0.14"
},
"ct3aMetadata": {
"initVersion": "7.39.3"
@@ -104,4 +104,4 @@
"overrides": {
"prismjs": "^1.30.0"
}
}
}

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2021-2026 community-scripts ORG
# Copyright (c) 2021-2025 community-scripts ORG
# Author: tteck (tteckster)
# Co-Author: MickLesk
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
@@ -11,9 +11,6 @@ source "$(dirname "${BASH_SOURCE[0]}")/error-handler.func"
load_functions
catch_errors
# Get LXC IP address (must be called INSIDE container, after network is up)
get_lxc_ip
# This function enables IPv6 if it's not disabled and sets verbose mode
verb_ip6() {
set_std_mode # Set STD mode based on VERBOSE
@@ -128,13 +125,22 @@ update_os() {
# This function modifies the message of the day (motd) and SSH settings
motd_ssh() {
echo "export TERM='xterm-256color'" >>/root/.bashrc
IP=$(ip -4 addr show eth0 | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1)
if [ -f "/etc/os-release" ]; then
OS_NAME=$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '"')
OS_VERSION=$(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '"')
else
OS_NAME="Alpine Linux"
OS_VERSION="Unknown"
fi
PROFILE_FILE="/etc/profile.d/00_lxc-details.sh"
echo "echo -e \"\"" >"$PROFILE_FILE"
echo -e "echo -e \"${BOLD}${APPLICATION} LXC Container${CL}"\" >>"$PROFILE_FILE"
echo -e "echo -e \"${TAB}${GATEWAY}${YW} Provided by: ${GN}community-scripts ORG ${YW}| GitHub: ${GN}https://github.com/community-scripts/ProxmoxVE${CL}\"" >>"$PROFILE_FILE"
echo "echo \"\"" >>"$PROFILE_FILE"
echo -e "echo -e \"${TAB}${OS}${YW} OS: ${GN}\$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '\"') - Version: \$(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '\"')${CL}\"" >>"$PROFILE_FILE"
echo -e "echo -e \"${TAB}${OS}${YW} OS: ${GN}${OS_NAME} - Version: ${OS_VERSION}${CL}\"" >>"$PROFILE_FILE"
echo -e "echo -e \"${TAB}${HOSTNAME}${YW} Hostname: ${GN}\$(hostname)${CL}\"" >>"$PROFILE_FILE"
echo -e "echo -e \"${TAB}${INFO}${YW} IP Address: ${GN}\$(ip -4 addr show eth0 | awk '/inet / {print \$2}' | cut -d/ -f1 | head -n 1)${CL}\"" >>"$PROFILE_FILE"

View File

@@ -1,188 +1,507 @@
#!/bin/ash
# shellcheck shell=ash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
if ! command -v curl >/dev/null 2>&1; then
apk update && apk add curl >/dev/null 2>&1
fi
source "$(dirname "${BASH_SOURCE[0]}")/core.func"
source "$(dirname "${BASH_SOURCE[0]}")/error-handler.func"
load_functions
catch_errors
# Expects existing msg_* functions and optional $STD from the framework.
# Get LXC IP address (must be called INSIDE container, after network is up)
get_lxc_ip
# ------------------------------
# helpers
# ------------------------------
lower() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]'; }
has() { command -v "$1" >/dev/null 2>&1; }
# This function enables IPv6 if it's not disabled and sets verbose mode
verb_ip6() {
set_std_mode # Set STD mode based on VERBOSE
if [ "${IPV6_METHOD:-}" = "disable" ]; then
msg_info "Disabling IPv6 (this may affect some services)"
$STD sysctl -w net.ipv6.conf.all.disable_ipv6=1
$STD sysctl -w net.ipv6.conf.default.disable_ipv6=1
$STD sysctl -w net.ipv6.conf.lo.disable_ipv6=1
mkdir -p /etc/sysctl.d
$STD tee /etc/sysctl.d/99-disable-ipv6.conf >/dev/null <<EOF
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.lo.disable_ipv6 = 1
EOF
$STD rc-update add sysctl default
msg_ok "Disabled IPv6"
need_tool() {
# usage: need_tool curl jq unzip ...
# setup missing tools via apk
local missing=0 t
for t in "$@"; do
if ! has "$t"; then missing=1; fi
done
if [ "$missing" -eq 1 ]; then
msg_info "Installing tools: $*"
apk add --no-cache "$@" >/dev/null 2>&1 || {
msg_error "apk add failed for: $*"
return 1
}
msg_ok "Tools ready: $*"
fi
}
set -Eeuo pipefail
trap 'error_handler $? $LINENO "$BASH_COMMAND"' ERR
trap on_exit EXIT
trap on_interrupt INT
trap on_terminate TERM
net_resolves() {
# better handling for missing getent on Alpine
# usage: net_resolves api.github.com
local host="$1"
ping -c1 -W1 "$host" >/dev/null 2>&1 || nslookup "$host" >/dev/null 2>&1
}
error_handler() {
local exit_code="$1"
local line_number="$2"
local command="$3"
ensure_usr_local_bin_persist() {
local PROFILE_FILE="/etc/profile.d/10-localbin.sh"
if [ ! -f "$PROFILE_FILE" ]; then
echo 'case ":$PATH:" in *:/usr/local/bin:*) ;; *) export PATH="/usr/local/bin:$PATH";; esac' >"$PROFILE_FILE"
chmod +x "$PROFILE_FILE"
fi
}
if [[ "$exit_code" -eq 0 ]]; then
download_with_progress() {
# $1 url, $2 dest
local url="$1" out="$2" cl
need_tool curl pv || return 1
cl=$(curl -fsSLI "$url" 2>/dev/null | awk 'tolower($0) ~ /^content-length:/ {print $2}' | tr -d '\r')
if [ -n "$cl" ]; then
curl -fsSL "$url" | pv -s "$cl" >"$out" || {
msg_error "Download failed: $url"
return 1
}
else
curl -fL# -o "$out" "$url" || {
msg_error "Download failed: $url"
return 1
}
fi
}
# ------------------------------
# GitHub: check Release
# ------------------------------
check_for_gh_release() {
# app, repo, [pinned]
local app="$1" source="$2" pinned="${3:-}"
local app_lc
app_lc="$(lower "$app" | tr -d ' ')"
local current_file="$HOME/.${app_lc}"
local current="" release tag
msg_info "Check for update: $app"
net_resolves api.github.com || {
msg_error "DNS/network error: api.github.com"
return 1
}
need_tool curl jq || return 1
tag=$(curl -fsSL "https://api.github.com/repos/${source}/releases/latest" | jq -r '.tag_name // empty')
[ -z "$tag" ] && {
msg_error "Unable to fetch latest tag for $app"
return 1
}
release="${tag#v}"
[ -f "$current_file" ] && current="$(cat "$current_file")"
if [ -n "$pinned" ]; then
if [ "$pinned" = "$release" ]; then
msg_ok "$app pinned to v$pinned (no update)"
return 1
fi
if [ "$current" = "$pinned" ]; then
msg_ok "$app pinned v$pinned installed (upstream v$release)"
return 1
fi
msg_info "$app pinned v$pinned (upstream v$release) → update/downgrade"
CHECK_UPDATE_RELEASE="$pinned"
return 0
fi
printf "\e[?25h"
echo -e "\n${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}\n"
exit "$exit_code"
}
on_exit() {
local exit_code="$?"
[[ -n "${lockfile:-}" && -e "$lockfile" ]] && rm -f "$lockfile"
exit "$exit_code"
}
on_interrupt() {
echo -e "\n${RD}Interrupted by user (SIGINT)${CL}"
exit 130
}
on_terminate() {
echo -e "\n${RD}Terminated by signal (SIGTERM)${CL}"
exit 143
}
# This function sets up the Container OS by generating the locale, setting the timezone, and checking the network connection
setting_up_container() {
msg_info "Setting up Container OS"
while [ $i -gt 0 ]; do
if [ "$(ip addr show | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | cut -d'/' -f1)" != "" ]; then
break
fi
echo 1>&2 -en "${CROSS}${RD} No Network! "
sleep $RETRY_EVERY
i=$((i - 1))
done
if [ "$(ip addr show | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | cut -d'/' -f1)" = "" ]; then
echo 1>&2 -e "\n${CROSS}${RD} No Network After $RETRY_NUM Tries${CL}"
echo -e "${NETWORK}Check Network Settings"
exit 1
if [ "$release" != "$current" ] || [ ! -f "$current_file" ]; then
CHECK_UPDATE_RELEASE="$release"
msg_info "New release available: v$release (current: v${current:-none})"
return 0
fi
msg_ok "Set up Container OS"
msg_ok "Network Connected: ${BL}$(ip addr show | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1 | tail -n1)${CL}"
msg_ok "$app is up to date (v$release)"
return 1
}
# This function checks the network connection by pinging a known IP address and prompts the user to continue if the internet is not connected
network_check() {
set +e
trap - ERR
if ping -c 1 -W 1 1.1.1.1 &>/dev/null || ping -c 1 -W 1 8.8.8.8 &>/dev/null || ping -c 1 -W 1 9.9.9.9 &>/dev/null; then
ipv4_status="${GN}✔${CL} IPv4"
# ------------------------------
# GitHub: get Release & deploy (Alpine)
# modes: tarball | prebuild | singlefile
# ------------------------------
fetch_and_deploy_gh() {
# $1 app, $2 repo, [$3 mode], [$4 version], [$5 target], [$6 asset_pattern
local app="$1" repo="$2" mode="${3:-tarball}" version="${4:-latest}" target="${5:-/opt/$1}" pattern="${6:-}"
local app_lc
app_lc="$(lower "$app" | tr -d ' ')"
local vfile="$HOME/.${app_lc}"
local json url filename tmpd unpack
net_resolves api.github.com || {
msg_error "DNS/network error"
return 1
}
need_tool curl jq tar || return 1
[ "$mode" = "prebuild" ] || [ "$mode" = "singlefile" ] && need_tool unzip >/dev/null 2>&1 || true
tmpd="$(mktemp -d)" || return 1
mkdir -p "$target"
# Release JSON
if [ "$version" = "latest" ]; then
json="$(curl -fsSL "https://api.github.com/repos/$repo/releases/latest")" || {
msg_error "GitHub API failed"
rm -rf "$tmpd"
return 1
}
else
ipv4_status="${RD}✖${CL} IPv4"
read -r -p "Internet NOT connected. Continue anyway? <y/N> " prompt
if [[ "${prompt,,}" =~ ^(y|yes)$ ]]; then
echo -e "${INFO}${RD}Expect Issues Without Internet${CL}"
json="$(curl -fsSL "https://api.github.com/repos/$repo/releases/tags/$version")" || {
msg_error "GitHub API failed"
rm -rf "$tmpd"
return 1
}
fi
# correct Version
version="$(printf '%s' "$json" | jq -r '.tag_name // empty')"
version="${version#v}"
[ -z "$version" ] && {
msg_error "No tag in release json"
rm -rf "$tmpd"
return 1
}
case "$mode" in
tarball | source)
url="$(printf '%s' "$json" | jq -r '.tarball_url // empty')"
[ -z "$url" ] && url="https://github.com/$repo/archive/refs/tags/v$version.tar.gz"
filename="${app_lc}-${version}.tar.gz"
download_with_progress "$url" "$tmpd/$filename" || {
rm -rf "$tmpd"
return 1
}
tar -xzf "$tmpd/$filename" -C "$tmpd" || {
msg_error "tar extract failed"
rm -rf "$tmpd"
return 1
}
unpack="$(find "$tmpd" -mindepth 1 -maxdepth 1 -type d | head -n1)"
# copy content of unpack to target
(cd "$unpack" && tar -cf - .) | (cd "$target" && tar -xf -) || {
msg_error "copy failed"
rm -rf "$tmpd"
return 1
}
;;
prebuild)
[ -n "$pattern" ] || {
msg_error "prebuild requires asset pattern"
rm -rf "$tmpd"
return 1
}
url="$(printf '%s' "$json" | jq -r '.assets[].browser_download_url' | awk -v p="$pattern" '
BEGIN{IGNORECASE=1}
$0 ~ p {print; exit}
')"
[ -z "$url" ] && {
msg_error "asset not found for pattern: $pattern"
rm -rf "$tmpd"
return 1
}
filename="${url##*/}"
download_with_progress "$url" "$tmpd/$filename" || {
rm -rf "$tmpd"
return 1
}
# unpack archive (Zip or tarball)
case "$filename" in
*.zip)
need_tool unzip || {
rm -rf "$tmpd"
return 1
}
mkdir -p "$tmpd/unp"
unzip -q "$tmpd/$filename" -d "$tmpd/unp"
;;
*.tar.gz | *.tgz | *.tar.xz | *.tar.zst | *.tar.bz2)
mkdir -p "$tmpd/unp"
tar -xf "$tmpd/$filename" -C "$tmpd/unp"
;;
*)
msg_error "unsupported archive: $filename"
rm -rf "$tmpd"
return 1
;;
esac
# top-level folder strippen
if [ "$(find "$tmpd/unp" -mindepth 1 -maxdepth 1 -type d | wc -l)" -eq 1 ] && [ -z "$(find "$tmpd/unp" -mindepth 1 -maxdepth 1 -type f | head -n1)" ]; then
unpack="$(find "$tmpd/unp" -mindepth 1 -maxdepth 1 -type d)"
(cd "$unpack" && tar -cf - .) | (cd "$target" && tar -xf -) || {
msg_error "copy failed"
rm -rf "$tmpd"
return 1
}
else
echo -e "${NETWORK}Check Network Settings"
exit 1
(cd "$tmpd/unp" && tar -cf - .) | (cd "$target" && tar -xf -) || {
msg_error "copy failed"
rm -rf "$tmpd"
return 1
}
fi
;;
singlefile)
[ -n "$pattern" ] || {
msg_error "singlefile requires asset pattern"
rm -rf "$tmpd"
return 1
}
url="$(printf '%s' "$json" | jq -r '.assets[].browser_download_url' | awk -v p="$pattern" '
BEGIN{IGNORECASE=1}
$0 ~ p {print; exit}
')"
[ -z "$url" ] && {
msg_error "asset not found for pattern: $pattern"
rm -rf "$tmpd"
return 1
}
filename="${url##*/}"
download_with_progress "$url" "$target/$app" || {
rm -rf "$tmpd"
return 1
}
chmod +x "$target/$app"
;;
*)
msg_error "Unknown mode: $mode"
rm -rf "$tmpd"
return 1
;;
esac
echo "$version" >"$vfile"
ensure_usr_local_bin_persist
rm -rf "$tmpd"
msg_ok "Deployed $app ($version) → $target"
}
# ------------------------------
# yq (mikefarah) Alpine
# ------------------------------
setup_yq() {
# prefer apk, unless FORCE_GH=1
if [ "${FORCE_GH:-0}" != "1" ] && apk info -e yq >/dev/null 2>&1; then
msg_info "Updating yq via apk"
apk add --no-cache --upgrade yq >/dev/null 2>&1 || true
msg_ok "yq ready ($(yq --version 2>/dev/null))"
return 0
fi
need_tool curl || return 1
local arch bin url tmp
case "$(uname -m)" in
x86_64) arch="amd64" ;;
aarch64) arch="arm64" ;;
*)
msg_error "Unsupported arch for yq: $(uname -m)"
return 1
;;
esac
url="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${arch}"
tmp="$(mktemp)"
download_with_progress "$url" "$tmp" || return 1
install -m 0755 "$tmp" /usr/local/bin/yq
rm -f "$tmp"
msg_ok "Setup yq ($(yq --version 2>/dev/null))"
}
# ------------------------------
# Adminer Alpine
# ------------------------------
setup_adminer() {
need_tool curl || return 1
msg_info "Setup Adminer (Alpine)"
mkdir -p /var/www/localhost/htdocs/adminer
curl -fsSL https://github.com/vrana/adminer/releases/latest/download/adminer.php \
-o /var/www/localhost/htdocs/adminer/index.php || {
msg_error "Adminer download failed"
return 1
}
msg_ok "Adminer at /adminer (served by your webserver)"
}
# ------------------------------
# uv Alpine (musl tarball)
# optional: PYTHON_VERSION="3.12"
# ------------------------------
setup_uv() {
need_tool curl tar || return 1
local UV_BIN="/usr/local/bin/uv"
local arch tarball url tmpd ver installed
case "$(uname -m)" in
x86_64) arch="x86_64-unknown-linux-musl" ;;
aarch64) arch="aarch64-unknown-linux-musl" ;;
*)
msg_error "Unsupported arch for uv: $(uname -m)"
return 1
;;
esac
ver="$(curl -fsSL https://api.github.com/repos/astral-sh/uv/releases/latest | jq -r '.tag_name' 2>/dev/null)"
ver="${ver#v}"
[ -z "$ver" ] && {
msg_error "uv: cannot determine latest version"
return 1
}
if has "$UV_BIN"; then
installed="$($UV_BIN -V 2>/dev/null | awk '{print $2}')"
[ "$installed" = "$ver" ] && {
msg_ok "uv $ver already installed"
return 0
}
msg_info "Updating uv $installed → $ver"
else
msg_info "Setup uv $ver"
fi
tmpd="$(mktemp -d)" || return 1
tarball="uv-${arch}.tar.gz"
url="https://github.com/astral-sh/uv/releases/download/v${ver}/${tarball}"
download_with_progress "$url" "$tmpd/uv.tar.gz" || {
rm -rf "$tmpd"
return 1
}
tar -xzf "$tmpd/uv.tar.gz" -C "$tmpd" || {
msg_error "uv: extract failed"
rm -rf "$tmpd"
return 1
}
# tar contains ./uv
if [ -x "$tmpd/uv" ]; then
install -m 0755 "$tmpd/uv" "$UV_BIN"
else
# fallback: in subfolder
install -m 0755 "$tmpd"/*/uv "$UV_BIN" 2>/dev/null || {
msg_error "uv binary not found in tar"
rm -rf "$tmpd"
return 1
}
fi
rm -rf "$tmpd"
ensure_usr_local_bin_persist
msg_ok "Setup uv $ver"
if [ -n "${PYTHON_VERSION:-}" ]; then
local match
match="$(uv python list --only-downloads 2>/dev/null | awk -v maj="$PYTHON_VERSION" '
$0 ~ "^cpython-"maj"\\." { print $0 }' | awk -F- '{print $2}' | sort -V | tail -n1)"
[ -z "$match" ] && {
msg_error "No matching Python for $PYTHON_VERSION"
return 1
}
if ! uv python list | grep -q "cpython-${match}-linux"; then
msg_info "Installing Python $match via uv"
uv python install "$match" || {
msg_error "uv python install failed"
return 1
}
msg_ok "Python $match installed (uv)"
fi
fi
RESOLVEDIP=$(getent hosts github.com | awk '{ print $1 }')
if [[ -z "$RESOLVEDIP" ]]; then
msg_error "Internet: ${ipv4_status} DNS Failed"
}
# ------------------------------
# Java Alpine (OpenJDK)
# JAVA_VERSION: 17|21 (Default 21)
# ------------------------------
setup_java() {
local JAVA_VERSION="${JAVA_VERSION:-21}" pkg
case "$JAVA_VERSION" in
17) pkg="openjdk17-jdk" ;;
21 | *) pkg="openjdk21-jdk" ;;
esac
msg_info "Setup Java (OpenJDK $JAVA_VERSION)"
apk add --no-cache "$pkg" >/dev/null 2>&1 || {
msg_error "apk add $pkg failed"
return 1
}
# set JAVA_HOME
local prof="/etc/profile.d/20-java.sh"
if [ ! -f "$prof" ]; then
echo 'export JAVA_HOME=$(dirname $(dirname $(readlink -f $(command -v java))))' >"$prof"
echo 'case ":$PATH:" in *:$JAVA_HOME/bin:*) ;; *) export PATH="$JAVA_HOME/bin:$PATH";; esac' >>"$prof"
chmod +x "$prof"
fi
msg_ok "Java ready: $(java -version 2>&1 | head -n1)"
}
# ------------------------------
# Go Alpine (apk prefers, else tarball)
# ------------------------------
setup_go() {
if [ -z "${GO_VERSION:-}" ]; then
msg_info "Setup Go (apk)"
apk add --no-cache go >/dev/null 2>&1 || {
msg_error "apk add go failed"
return 1
}
msg_ok "Go ready: $(go version 2>/dev/null)"
return 0
fi
need_tool curl tar || return 1
local ARCH TARBALL URL TMP
case "$(uname -m)" in
x86_64) ARCH="amd64" ;;
aarch64) ARCH="arm64" ;;
*)
msg_error "Unsupported arch for Go: $(uname -m)"
return 1
;;
esac
TARBALL="go${GO_VERSION}.linux-${ARCH}.tar.gz"
URL="https://go.dev/dl/${TARBALL}"
msg_info "Setup Go $GO_VERSION (tarball)"
TMP="$(mktemp)"
download_with_progress "$URL" "$TMP" || return 1
rm -rf /usr/local/go
tar -C /usr/local -xzf "$TMP" || {
msg_error "extract go failed"
rm -f "$TMP"
return 1
}
rm -f "$TMP"
ln -sf /usr/local/go/bin/go /usr/local/bin/go
ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt
ensure_usr_local_bin_persist
msg_ok "Go ready: $(go version 2>/dev/null)"
}
# ------------------------------
# Composer Alpine
# uses php83-cli + openssl + phar
# ------------------------------
setup_composer() {
local COMPOSER_BIN="/usr/local/bin/composer"
if ! has php; then
# prefers php83
msg_info "Installing PHP CLI for Composer"
apk add --no-cache php83-cli php83-openssl php83-phar php83-iconv >/dev/null 2>&1 || {
# Fallback to generic php if 83 not available
apk add --no-cache php-cli php-openssl php-phar php-iconv >/dev/null 2>&1 || {
msg_error "Failed to install php-cli for composer"
return 1
}
}
msg_ok "PHP CLI ready: $(php -v | head -n1)"
fi
if [ -x "$COMPOSER_BIN" ]; then
msg_info "Updating Composer"
else
msg_ok "Internet: ${ipv4_status} DNS: ${BL}${RESOLVEDIP}${CL}"
fi
set -e
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
}
# This function updates the Container OS by running apt-get update and upgrade
update_os() {
msg_info "Updating Container OS"
$STD apk -U upgrade
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func)
msg_ok "Updated Container OS"
}
# This function modifies the message of the day (motd) and SSH settings
motd_ssh() {
echo "export TERM='xterm-256color'" >>/root/.bashrc
PROFILE_FILE="/etc/profile.d/00_lxc-details.sh"
echo "echo -e \"\"" >"$PROFILE_FILE"
echo -e "echo -e \"${BOLD}${APPLICATION} LXC Container${CL}"\" >>"$PROFILE_FILE"
echo -e "echo -e \"${TAB}${GATEWAY}${YW} Provided by: ${GN}community-scripts ORG ${YW}| GitHub: ${GN}https://github.com/community-scripts/ProxmoxVE${CL}\"" >>"$PROFILE_FILE"
echo "echo \"\"" >>"$PROFILE_FILE"
echo -e "echo -e \"${TAB}${OS}${YW} OS: ${GN}\$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '\"') - Version: \$(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '\"')${CL}\"" >>"$PROFILE_FILE"
echo -e "echo -e \"${TAB}${HOSTNAME}${YW} Hostname: ${GN}\$(hostname)${CL}\"" >>"$PROFILE_FILE"
echo -e "echo -e \"${TAB}${INFO}${YW} IP Address: ${GN}\$(ip -4 addr show eth0 | awk '/inet / {print \$2}' | cut -d/ -f1 | head -n 1)${CL}\"" >>"$PROFILE_FILE"
# Configure SSH if enabled
if [[ "${SSH_ROOT}" == "yes" ]]; then
# Enable sshd service
$STD rc-update add sshd
# Allow root login via SSH
sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin yes/g" /etc/ssh/sshd_config
# Start the sshd service
$STD /etc/init.d/sshd start
fi
}
# Validate Timezone for some LXC's
validate_tz() {
[[ -f "/usr/share/zoneinfo/$1" ]]
}
# This function customizes the container and enables passwordless login for the root user
customize() {
if [[ "$PASSWORD" == "" ]]; then
msg_info "Customizing Container"
passwd -d root >/dev/null 2>&1
# Ensure agetty is available
apk add --no-cache --force-broken-world util-linux >/dev/null 2>&1
# Create persistent autologin boot script
mkdir -p /etc/local.d
cat <<'EOF' >/etc/local.d/autologin.start
#!/bin/sh
sed -i 's|^tty1::respawn:.*|tty1::respawn:/sbin/agetty --autologin root --noclear tty1 38400 linux|' /etc/inittab
kill -HUP 1
EOF
touch /root/.hushlogin
chmod +x /etc/local.d/autologin.start
rc-update add local >/dev/null 2>&1
# Apply autologin immediately for current session
/etc/local.d/autologin.start
msg_ok "Customized Container"
msg_info "Setup Composer"
fi
echo "bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/${app}.sh)\"" >/usr/bin/update
chmod +x /usr/bin/update
need_tool curl || return 1
curl -fsSL https://getcomposer.org/installer -o /tmp/composer-setup.php || {
msg_error "composer installer download failed"
return 1
}
php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer >/dev/null 2>&1 || {
msg_error "composer install failed"
return 1
}
rm -f /tmp/composer-setup.php
ensure_usr_local_bin_persist
msg_ok "Composer ready: $(composer --version 2>/dev/null)"
}

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2021-2026 community-scripts ORG
# Copyright (c) 2021-2025 community-scripts ORG
# Author: michelroegl-brunner
# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/LICENSE

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Copyright (c) 2021-2025 community-scripts ORG
# Author: community-scripts ORG
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/branch/main/LICENSE
# Revision: 1
@@ -502,4 +502,4 @@ if validate_ip_cidr "192.168.1.100/24"; then
echo "Valid IP/CIDR"
fi
EXAMPLES
EXAMPLES

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Copyright (c) 2021-2025 community-scripts ORG
# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/LICENSE
# ==============================================================================
@@ -123,38 +123,9 @@ icons() {
CREATING="${TAB}🚀${TAB}${CL}"
ADVANCED="${TAB}🧩${TAB}${CL}"
FUSE="${TAB}🗂️${TAB}${CL}"
GPU="${TAB}🎮${TAB}${CL}"
HOURGLASS="${TAB}⏳${TAB}"
}
# ------------------------------------------------------------------------------
# ensure_profile_loaded()
#
# - Sources /etc/profile.d/*.sh scripts if not already loaded
# - Fixes PATH issues when running via pct enter/exec (non-login shells)
# - Safe to call multiple times (uses guard variable)
# - Should be called in update_script() or any script running inside LXC
# ------------------------------------------------------------------------------
ensure_profile_loaded() {
# Skip if already loaded or running on Proxmox host
[[ -n "${_PROFILE_LOADED:-}" ]] && return
command -v pveversion &>/dev/null && return
# Source all profile.d scripts to ensure PATH is complete
if [[ -d /etc/profile.d ]]; then
for script in /etc/profile.d/*.sh; do
[[ -r "$script" ]] && source "$script"
done
fi
# Also ensure /usr/local/bin is in PATH (common install location)
if [[ ":$PATH:" != *":/usr/local/bin:"* ]]; then
export PATH="/usr/local/bin:$PATH"
fi
export _PROFILE_LOADED=1
}
# ------------------------------------------------------------------------------
# default_vars()
#
@@ -815,9 +786,11 @@ is_verbose_mode() {
# ------------------------------------------------------------------------------
# cleanup_lxc()
#
# - Cleans package manager and language caches (safe for installs AND updates)
# - Supports Alpine (apk), Debian/Ubuntu (apt), Python, Node.js, Go, Rust, Ruby, PHP
# - Uses fallback error handling to prevent cleanup failures from breaking installs
# - Comprehensive cleanup of package managers, caches, and logs
# - Supports Alpine (apk), Debian/Ubuntu (apt), and language package managers
# - Cleans: Python (pip/uv), Node.js (npm/yarn/pnpm), Go, Rust, Ruby, PHP
# - Truncates log files and vacuums systemd journal
# - Run at end of container creation to minimize disk usage
# ------------------------------------------------------------------------------
cleanup_lxc() {
msg_info "Cleaning up"
@@ -826,53 +799,39 @@ cleanup_lxc() {
$STD apk cache clean || true
rm -rf /var/cache/apk/*
else
$STD apt -y autoremove 2>/dev/null || msg_warn "apt autoremove failed (non-critical)"
$STD apt -y autoclean 2>/dev/null || msg_warn "apt autoclean failed (non-critical)"
$STD apt -y clean 2>/dev/null || msg_warn "apt clean failed (non-critical)"
$STD apt -y autoremove || true
$STD apt -y autoclean || true
$STD apt -y clean || true
fi
# Clear temp artifacts (keep sockets/FIFOs; ignore errors)
find /tmp /var/tmp -type f -name 'tmp*' -delete 2>/dev/null || true
find /tmp /var/tmp -type f -name 'tempfile*' -delete 2>/dev/null || true
# Python
if command -v pip &>/dev/null; then
rm -rf /root/.cache/pip 2>/dev/null || true
fi
if command -v uv &>/dev/null; then
rm -rf /root/.cache/uv 2>/dev/null || true
# Truncate writable log files silently (permission errors ignored)
if command -v truncate >/dev/null 2>&1; then
find /var/log -type f -writable -print0 2>/dev/null |
xargs -0 -n1 truncate -s 0 2>/dev/null || true
fi
# Node.js
if command -v npm &>/dev/null; then
rm -rf /root/.npm/_cacache /root/.npm/_logs 2>/dev/null || true
fi
if command -v yarn &>/dev/null; then
rm -rf /root/.cache/yarn /root/.yarn/cache 2>/dev/null || true
fi
if command -v pnpm &>/dev/null; then
pnpm store prune &>/dev/null || true
fi
# Node.js npm
if command -v npm &>/dev/null; then $STD npm cache clean --force || true; fi
# Node.js yarn
if command -v yarn &>/dev/null; then $STD yarn cache clean || true; fi
# Node.js pnpm
if command -v pnpm &>/dev/null; then $STD pnpm store prune || true; fi
# Go
if command -v go &>/dev/null; then $STD go clean -cache -modcache || true; fi
# Rust cargo
if command -v cargo &>/dev/null; then $STD cargo clean || true; fi
# Ruby gem
if command -v gem &>/dev/null; then $STD gem cleanup || true; fi
# Composer (PHP)
if command -v composer &>/dev/null; then $STD composer clear-cache || true; fi
# Go (only build cache, not modules)
if command -v go &>/dev/null; then
$STD go clean -cache 2>/dev/null || true
if command -v journalctl &>/dev/null; then
$STD journalctl --vacuum-time=10m || true
fi
# Rust (only registry cache, not build artifacts)
if command -v cargo &>/dev/null; then
rm -rf /root/.cargo/registry/cache /root/.cargo/.package-cache 2>/dev/null || true
fi
# Ruby
if command -v gem &>/dev/null; then
rm -rf /root/.gem/cache 2>/dev/null || true
fi
# PHP
if command -v composer &>/dev/null; then
rm -rf /root/.composer/cache 2>/dev/null || true
fi
msg_ok "Cleaned"
}
@@ -924,93 +883,6 @@ check_or_create_swap() {
fi
}
# ------------------------------------------------------------------------------
# Loads LOCAL_IP from persistent store or detects if missing.
#
# Description:
# - Loads from /run/local-ip.env or performs runtime lookup
# ------------------------------------------------------------------------------
function get_lxc_ip() {
local IP_FILE="/run/local-ip.env"
if [[ -f "$IP_FILE" ]]; then
# shellcheck disable=SC1090
source "$IP_FILE"
fi
if [[ -z "${LOCAL_IP:-}" ]]; then
get_current_ip() {
local ip
# Try direct interface lookup for eth0 FIRST (most reliable for LXC) - IPv4
ip=$(ip -4 addr show eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1 | head -n1)
if [[ -n "$ip" && "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "$ip"
return 0
fi
# Fallback: Try hostname -I (returns IPv4 first if available)
if command -v hostname >/dev/null 2>&1; then
ip=$(hostname -I 2>/dev/null | awk '{print $1}')
if [[ -n "$ip" && "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "$ip"
return 0
fi
fi
# Try routing table with IPv4 targets
local ipv4_targets=("8.8.8.8" "1.1.1.1" "default")
for target in "${ipv4_targets[@]}"; do
if [[ "$target" == "default" ]]; then
ip=$(ip route get 1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}')
else
ip=$(ip route get "$target" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}')
fi
if [[ -n "$ip" ]]; then
echo "$ip"
return 0
fi
done
# IPv6 fallback: Try direct interface lookup for eth0
ip=$(ip -6 addr show eth0 scope global 2>/dev/null | awk '/inet6 / {print $2}' | cut -d/ -f1 | head -n1)
if [[ -n "$ip" && "$ip" =~ : ]]; then
echo "$ip"
return 0
fi
# IPv6 fallback: Try hostname -I for IPv6
if command -v hostname >/dev/null 2>&1; then
ip=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E ':' | head -n1)
if [[ -n "$ip" && "$ip" =~ : ]]; then
echo "$ip"
return 0
fi
fi
# IPv6 fallback: Use routing table with IPv6 targets
local ipv6_targets=("2001:4860:4860::8888" "2606:4700:4700::1111")
for target in "${ipv6_targets[@]}"; do
ip=$(ip -6 route get "$target" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}')
if [[ -n "$ip" && "$ip" =~ : ]]; then
echo "$ip"
return 0
fi
done
return 1
}
LOCAL_IP="$(get_current_ip || true)"
if [[ -z "$LOCAL_IP" ]]; then
msg_error "Could not determine LOCAL_IP"
return 1
fi
fi
export LOCAL_IP
}
# ==============================================================================
# SIGNAL TRAPS
# ==============================================================================

View File

@@ -2,7 +2,7 @@
# ------------------------------------------------------------------------------
# ERROR HANDLER - ERROR & SIGNAL MANAGEMENT
# ------------------------------------------------------------------------------
# Copyright (c) 2021-2026 community-scripts ORG
# Copyright (c) 2021-2025 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# ------------------------------------------------------------------------------
@@ -34,9 +34,9 @@
# * Node.js/npm errors (243-249, 254)
# * Python/pip/uv errors (210-212)
# * PostgreSQL errors (231-234)
# * MySQL/MariaDB errors (241-244)
# * MongoDB errors (251-254)
# * Proxmox custom codes (200-231)
# * MySQL/MariaDB errors (260-263)
# * MongoDB errors (251-253)
# * Proxmox custom codes (200-209, 213-223, 225)
# - Returns description string for given exit code
# ------------------------------------------------------------------------------
explain_exit_code() {
@@ -319,4 +319,4 @@ catch_errors() {
trap on_exit EXIT
trap on_interrupt INT
trap on_terminate TERM
}
}

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2021-2026 community-scripts ORG
# Copyright (c) 2021-2025 community-scripts ORG
# Author: tteck (tteckster)
# Co-Author: MickLesk
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
@@ -37,9 +37,6 @@ source "$(dirname "${BASH_SOURCE[0]}")/error-handler.func"
load_functions
catch_errors
# Get LXC IP address (must be called INSIDE container, after network is up)
get_lxc_ip
# ==============================================================================
# SECTION 2: NETWORK & CONNECTIVITY
# ==============================================================================
@@ -79,13 +76,6 @@ EOF
# ------------------------------------------------------------------------------
setting_up_container() {
msg_info "Setting up Container OS"
# Fix Debian 13 LXC template bug where / is owned by nobody
# Only attempt in privileged containers (unprivileged cannot chown /)
if [[ "$(stat -c '%U' /)" != "root" ]]; then
(chown root:root / 2>/dev/null) || true
fi
for ((i = RETRY_NUM; i > 0; i--)); do
if [ "$(hostname -I)" != "" ]; then
break
@@ -232,12 +222,21 @@ motd_ssh() {
# Set terminal to 256-color mode
grep -qxF "export TERM='xterm-256color'" /root/.bashrc || echo "export TERM='xterm-256color'" >>/root/.bashrc
# Get OS information (Debian / Ubuntu)
if [ -f "/etc/os-release" ]; then
OS_NAME=$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '"')
OS_VERSION=$(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '"')
elif [ -f "/etc/debian_version" ]; then
OS_NAME="Debian"
OS_VERSION=$(cat /etc/debian_version)
fi
PROFILE_FILE="/etc/profile.d/00_lxc-details.sh"
echo "echo -e \"\"" >"$PROFILE_FILE"
echo -e "echo -e \"${BOLD}${APPLICATION} LXC Container${CL}"\" >>"$PROFILE_FILE"
echo -e "echo -e \"${TAB}${GATEWAY}${YW} Provided by: ${GN}community-scripts ORG ${YW}| GitHub: ${GN}https://github.com/community-scripts/ProxmoxVE${CL}\"" >>"$PROFILE_FILE"
echo "echo \"\"" >>"$PROFILE_FILE"
echo -e "echo -e \"${TAB}${OS}${YW} OS: ${GN}\$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '\"') - Version: \$(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '\"')${CL}\"" >>"$PROFILE_FILE"
echo -e "echo -e \"${TAB}${OS}${YW} OS: ${GN}${OS_NAME} - Version: ${OS_VERSION}${CL}\"" >>"$PROFILE_FILE"
echo -e "echo -e \"${TAB}${HOSTNAME}${YW} Hostname: ${GN}\$(hostname)${CL}\"" >>"$PROFILE_FILE"
echo -e "echo -e \"${TAB}${INFO}${YW} IP Address: ${GN}\$(hostname -I | awk '{print \$1}')${CL}\"" >>"$PROFILE_FILE"

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
SCRIPT_DIR="$(dirname "$0")"
source "$SCRIPT_DIR/../core/build.func"
# Copyright (c) 2021-2026 tteck
# Copyright (c) 2021-2025 tteck
# Author: tteck (tteckster)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://www.debian.org/
@@ -40,5 +40,5 @@ start
build_container
description
msg_ok "Completed successfully!\n"
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 tteck
# Copyright (c) 2021-2025 tteck
# Author: tteck (tteckster)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://www.debian.org/

View File

@@ -1610,7 +1610,6 @@ class ScriptExecutionHandler {
// TerminalHandler removed - not used by current application
app.prepare().then(() => {
console.log('> Next.js app prepared successfully');
const httpServer = createServer(async (req, res) => {
try {
// Be sure to pass `true` as the second argument to `url.parse`.
@@ -1716,9 +1715,4 @@ app.prepare().then(() => {
autoSyncModule.setupGracefulShutdown();
}
});
}).catch((err) => {
console.error('> Failed to start server:', err.message);
console.error('> If you see "Could not find a production build", run: npm run build');
console.error('> Full error:', err);
process.exit(1);
});

View File

@@ -58,11 +58,6 @@ export function ConfigurationModal({
// Advanced mode state
const [advancedVars, setAdvancedVars] = useState<EnvVars>({});
// Discovered SSH keys on the Proxmox host (advanced mode only)
const [discoveredSshKeys, setDiscoveredSshKeys] = useState<string[]>([]);
const [discoveredSshKeysLoading, setDiscoveredSshKeysLoading] = useState(false);
const [discoveredSshKeysError, setDiscoveredSshKeysError] = useState<string | null>(null);
// Validation errors
const [errors, setErrors] = useState<Record<string, string>>({});
@@ -124,38 +119,6 @@ export function ConfigurationModal({
}
}, [actualScript, server, mode, resources, slug]);
// Discover SSH keys on the Proxmox host when advanced mode is open
useEffect(() => {
if (!server?.id || !isOpen || mode !== 'advanced') {
setDiscoveredSshKeys([]);
setDiscoveredSshKeysError(null);
return;
}
let cancelled = false;
setDiscoveredSshKeysLoading(true);
setDiscoveredSshKeysError(null);
fetch(`/api/servers/${server.id}/discover-ssh-keys`)
.then((res) => {
if (!res.ok) throw new Error(res.status === 404 ? 'Server not found' : res.statusText);
return res.json();
})
.then((data: { keys?: string[] }) => {
if (!cancelled && Array.isArray(data.keys)) setDiscoveredSshKeys(data.keys);
})
.catch((err) => {
if (!cancelled) {
setDiscoveredSshKeys([]);
setDiscoveredSshKeysError(err instanceof Error ? err.message : 'Could not detect keys');
}
})
.finally(() => {
if (!cancelled) setDiscoveredSshKeysLoading(false);
});
return () => {
cancelled = true;
};
}, [server?.id, isOpen, mode]);
// Validation functions
const validateIPv4 = (ip: string): boolean => {
if (!ip) return true; // Empty is allowed (auto)
@@ -312,16 +275,6 @@ export function ConfigurationModal({
if ((hasPassword || hasSSHKey) && envVars.var_ssh !== 'no') {
envVars.var_ssh = 'yes';
}
// Normalize var_tags: accept both comma and semicolon, output comma-separated
const rawTags = envVars.var_tags;
if (typeof rawTags === 'string' && rawTags.trim() !== '') {
envVars.var_tags = rawTags
.split(/[,;]/)
.map((s) => s.trim())
.filter(Boolean)
.join(',');
}
}
// Remove empty string values (but keep 0, false, etc.)
@@ -691,13 +644,13 @@ export function ConfigurationModal({
</div>
<div className="col-span-2">
<label className="block text-sm font-medium text-foreground mb-2">
Tags (comma or semicolon separated)
Tags (comma-separated)
</label>
<Input
type="text"
value={typeof advancedVars.var_tags === 'boolean' ? '' : String(advancedVars.var_tags ?? '')}
onChange={(e) => updateAdvancedVar('var_tags', e.target.value)}
placeholder="e.g. tag1; tag2"
placeholder="community-script"
/>
</div>
</div>
@@ -724,40 +677,11 @@ export function ConfigurationModal({
<label className="block text-sm font-medium text-foreground mb-2">
SSH Authorized Key
</label>
{discoveredSshKeysLoading && (
<p className="text-sm text-muted-foreground mb-2">Detecting SSH keys...</p>
)}
{discoveredSshKeysError && !discoveredSshKeysLoading && (
<p className="text-sm text-muted-foreground mb-2">Could not detect keys on host</p>
)}
{discoveredSshKeys.length > 0 && !discoveredSshKeysLoading && (
<div className="mb-2">
<label htmlFor="discover-ssh-key" className="sr-only">Use detected key</label>
<select
id="discover-ssh-key"
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none mb-2"
value=""
onChange={(e) => {
const idx = e.target.value;
if (idx === '') return;
const key = discoveredSshKeys[Number(idx)];
if (key) updateAdvancedVar('var_ssh_authorized_key', key);
}}
>
<option value=""> Select or paste below </option>
{discoveredSshKeys.map((key, i) => (
<option key={i} value={i}>
{key.length > 44 ? `${key.slice(0, 44)}...` : key}
</option>
))}
</select>
</div>
)}
<Input
type="text"
value={typeof advancedVars.var_ssh_authorized_key === 'boolean' ? '' : String(advancedVars.var_ssh_authorized_key ?? '')}
onChange={(e) => updateAdvancedVar('var_ssh_authorized_key', e.target.value)}
placeholder="Or paste a public key: ssh-rsa AAAA..."
placeholder="ssh-rsa AAAA..."
/>
</div>
</div>

View File

@@ -16,7 +16,7 @@ export function Footer({ onOpenReleaseNotes }: FooterProps) {
<div className="container mx-auto px-4">
<div className="flex flex-col sm:flex-row items-center justify-between gap-2 text-sm text-muted-foreground">
<div className="flex items-center gap-2">
<span>© 2026 PVE Scripts Local</span>
<span>© 2024 PVE Scripts Local</span>
{versionData?.success && versionData.version && (
<Button
variant="ghost"

View File

@@ -416,20 +416,11 @@ export function VersionDisplay({ onOpenReleaseNotes }: VersionDisplayProps = {})
setShowUpdateConfirmation(true);
};
// Helper to generate secure random string
function getSecureRandomString(length: number): string {
const array = new Uint8Array(length);
window.crypto.getRandomValues(array);
// Convert to base36 string (alphanumeric)
return Array.from(array, b => b.toString(36)).join('').substr(0, length);
}
const handleConfirmUpdate = () => {
// Close the confirmation modal
setShowUpdateConfirmation(false);
// Start the actual update process
const randomSuffix = getSecureRandomString(9);
const sessionId = `update_${Date.now()}_${randomSuffix}`;
const sessionId = `update_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const startTime = Date.now();
setIsUpdating(true);

View File

@@ -1,96 +0,0 @@
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { getDatabase } from '../../../../../server/database-prisma';
import { getSSHExecutionService } from '../../../../../server/ssh-execution-service';
import type { Server } from '~/types/server';
const DISCOVER_TIMEOUT_MS = 10_000;
/** Match lines that look like SSH public keys (same as build.func) */
const SSH_PUBKEY_RE = /^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))\s+/;
/**
* Run a command on the Proxmox host and return buffered stdout.
* Resolves when the process exits or rejects on timeout/spawn error.
*/
function runRemoteCommand(
server: Server,
command: string,
timeoutMs: number
): Promise<{ stdout: string; exitCode: number }> {
const ssh = getSSHExecutionService();
return new Promise((resolve, reject) => {
const chunks: string[] = [];
let settled = false;
const finish = (stdout: string, exitCode: number) => {
if (settled) return;
settled = true;
clearTimeout(timer);
resolve({ stdout, exitCode });
};
const timer = setTimeout(() => {
if (settled) return;
settled = true;
reject(new Error('SSH discover keys timeout'));
}, timeoutMs);
ssh
.executeCommand(
server,
command,
(data: string) => chunks.push(data),
() => {},
(code: number) => finish(chunks.join(''), code)
)
.catch((err) => {
if (!settled) {
settled = true;
clearTimeout(timer);
reject(err);
}
});
});
}
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id: idParam } = await params;
const id = parseInt(idParam);
if (isNaN(id)) {
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 });
}
const db = getDatabase();
const server = await db.getServerById(id) as Server | null;
if (!server) {
return NextResponse.json({ error: 'Server not found' }, { status: 404 });
}
// Same paths as native build.func ssh_discover_default_files()
const remoteScript = `bash -c 'for f in /root/.ssh/authorized_keys /root/.ssh/authorized_keys2 /root/.ssh/*.pub /etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/* 2>/dev/null; do [ -f "$f" ] && [ -r "$f" ] && grep -E "^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-)" "$f" 2>/dev/null; done | sort -u'`;
const { stdout } = await runRemoteCommand(server, remoteScript, DISCOVER_TIMEOUT_MS);
const keys = stdout
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0 && SSH_PUBKEY_RE.test(line));
return NextResponse.json({ keys });
} catch (error) {
console.error('Error discovering SSH keys:', error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : String(error),
},
{ status: 500 }
);
}
}

View File

@@ -238,27 +238,6 @@ export const versionRouter = createTRPCRouter({
// Clear/create the log file
await writeFile(logPath, '', 'utf-8');
// Always fetch the latest update.sh from GitHub before running
// This ensures we always use the newest update script, avoiding
// the "chicken-and-egg" problem where old scripts can't update properly
const updateScriptUrl = 'https://raw.githubusercontent.com/community-scripts/ProxmoxVE-Local/main/update.sh';
try {
const response = await fetch(updateScriptUrl);
if (response.ok) {
const latestScript = await response.text();
await writeFile(updateScriptPath, latestScript, { mode: 0o755 });
// Log that we fetched the latest script
await writeFile(logPath, '[INFO] Fetched latest update.sh from GitHub\n', { flag: 'a' });
} else {
// If fetch fails, log warning but continue with local script
await writeFile(logPath, `[WARNING] Could not fetch latest update.sh (HTTP ${response.status}), using local version\n`, { flag: 'a' });
}
} catch (fetchError) {
// If fetch fails, log warning but continue with local script
const errorMsg = fetchError instanceof Error ? fetchError.message : 'Unknown error';
await writeFile(logPath, `[WARNING] Could not fetch latest update.sh: ${errorMsg}, using local version\n`, { flag: 'a' });
}
// Spawn the update script as a detached process using nohup
// This allows it to run independently and kill the parent Node.js process
// Redirect output to log file

View File

@@ -1,22 +1,9 @@
import 'dotenv/config'
import { PrismaClient } from '../../prisma/generated/prisma/client.ts'
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'
import { existsSync, mkdirSync } from 'fs'
import { dirname } from 'path'
const globalForPrisma = globalThis;
// Ensure database directory exists before initializing Prisma
// DATABASE_URL format: file:/path/to/database.db
const dbUrl = process.env.DATABASE_URL || 'file:./data/settings.db';
const dbPath = dbUrl.replace(/^file:/, '');
const dbDir = dirname(dbPath);
if (!existsSync(dbDir)) {
console.log(`Creating database directory: ${dbDir}`);
mkdirSync(dbDir, { recursive: true });
}
const adapter = new PrismaBetterSqlite3({ url: process.env.DATABASE_URL });
export const prisma = globalForPrisma.prisma ?? new PrismaClient({ adapter });

View File

@@ -1,22 +1,9 @@
import 'dotenv/config'
import { PrismaClient } from '../../prisma/generated/prisma/client'
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'
import { existsSync, mkdirSync } from 'fs'
import { dirname } from 'path'
const globalForPrisma = globalThis as { prisma?: PrismaClient };
// Ensure database directory exists before initializing Prisma
// DATABASE_URL format: file:/path/to/database.db
const dbUrl = process.env.DATABASE_URL || 'file:./data/settings.db';
const dbPath = dbUrl.replace(/^file:/, '');
const dbDir = dirname(dbPath);
if (!existsSync(dbDir)) {
console.log(`Creating database directory: ${dbDir}`);
mkdirSync(dbDir, { recursive: true });
}
const adapter = new PrismaBetterSqlite3({ url: process.env.DATABASE_URL! });
export const prisma: PrismaClient = globalForPrisma.prisma ?? new PrismaClient({

356
update.sh
View File

@@ -4,7 +4,7 @@
# 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
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
@@ -38,7 +38,7 @@ load_github_token() {
log "Using GitHub token from environment variable"
return 0
fi
# Try .env file
if [ -f ".env" ]; then
local env_token
@@ -49,21 +49,21 @@ load_github_token() {
return 0
fi
fi
# Try .github_token file
if [ -f ".github_token" ]; then
GITHUB_TOKEN=$(cat .github_token | tr -d '\n\r')
log "Using GitHub token from .github_token file"
return 0
fi
# Try ~/.github_token file
if [ -f "$HOME/.github_token" ]; then
GITHUB_TOKEN=$(cat "$HOME/.github_token" | tr -d '\n\r')
log "Using GitHub token from ~/.github_token file"
return 0
fi
log_warning "No GitHub token found. Using unauthenticated requests (lower rate limits)"
log_warning "To use a token, add GITHUB_TOKEN=your_token to .env file or set GITHUB_TOKEN environment variable"
return 1
@@ -72,7 +72,7 @@ load_github_token() {
# Initialize log file
init_log() {
# Clear/create log file
>"$LOG_FILE"
> "$LOG_FILE"
log "Starting ProxmoxVE-Local update process..."
log "Log file: $LOG_FILE"
}
@@ -97,40 +97,40 @@ log_warning() {
# Check if required tools are available
check_dependencies() {
log "Checking dependencies..."
local missing_deps=()
if ! command -v curl &>/dev/null; then
if ! command -v curl &> /dev/null; then
missing_deps+=("curl")
fi
if ! command -v jq &>/dev/null; then
if ! command -v jq &> /dev/null; then
missing_deps+=("jq")
fi
if ! command -v npm &>/dev/null; then
if ! command -v npm &> /dev/null; then
missing_deps+=("npm")
fi
if ! command -v node &>/dev/null; then
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 curl_opts="-s --connect-timeout 15 --max-time 60 --retry 2 --retry-delay 3"
# Add authentication header if token is available
if [ -n "$GITHUB_TOKEN" ]; then
curl_opts="$curl_opts -H \"Authorization: token $GITHUB_TOKEN\""
@@ -138,35 +138,35 @@ get_latest_release() {
else
log "Using unauthenticated GitHub API request (lower rate limits)"
fi
local release_info
if ! release_info=$(eval "curl $curl_opts \"$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"
}
@@ -174,16 +174,16 @@ get_latest_release() {
# Backup data directory, .env file, and scripts directories
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
@@ -193,7 +193,7 @@ backup_data() {
else
log_warning "Data directory not found, skipping backup"
fi
# Backup .env file
if [ -f ".env" ]; then
log "Backing up .env file..."
@@ -206,7 +206,7 @@ backup_data() {
else
log_warning ".env file not found, skipping backup"
fi
# Backup scripts directories
local scripts_dirs=("scripts/ct" "scripts/install" "scripts/tools" "scripts/vm")
for scripts_dir in "${scripts_dirs[@]}"; do
@@ -230,60 +230,60 @@ 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
if ! curl -L --connect-timeout 30 --max-time 300 --retry 3 --retry-delay 5 -o "$archive_file" "$download_url" 2>/dev/null; then
log_error "Failed to download release from GitHub"
rm -rf "$temp_dir"
exit 1
fi
# 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
log_success "Downloaded release"
# Extract release
if ! tar -xzf "$archive_file" -C "$temp_dir" 2>/dev/null; then
log_error "Failed to extract release"
rm -rf "$temp_dir"
exit 1
fi
# Find the extracted directory (GitHub tarballs have a root directory)
local extracted_dir
extracted_dir=$(find "$temp_dir" -maxdepth 1 -type d -name "community-scripts-ProxmoxVE-Local-*" 2>/dev/null | head -1)
# Try alternative patterns if not found
if [ -z "$extracted_dir" ]; then
extracted_dir=$(find "$temp_dir" -maxdepth 1 -type d -name "${REPO_NAME}-*" 2>/dev/null | head -1)
fi
if [ -z "$extracted_dir" ]; then
extracted_dir=$(find "$temp_dir" -maxdepth 1 -type d ! -name "$temp_dir" 2>/dev/null | head -1)
fi
if [ -z "$extracted_dir" ]; then
log_error "Could not find extracted directory"
rm -rf "$temp_dir"
exit 1
fi
log_success "Release extracted successfully"
echo "$extracted_dir"
}
@@ -291,11 +291,11 @@ download_release() {
# Clear the original directory before updating
clear_original_directory() {
log "Clearing original directory..."
# Remove old lock files and node_modules before update
rm -f package-lock.json 2>/dev/null
rm -rf node_modules 2>/dev/null
# List of files/directories to preserve (already backed up)
local preserve_patterns=(
"data"
@@ -308,48 +308,48 @@ clear_original_directory() {
".git"
"scripts"
)
# 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, data directory, and scripts directories from backup..."
if [ -d "$BACKUP_DIR" ]; then
# Restore .env file
if [ -f "$BACKUP_DIR/.env" ]; then
@@ -365,7 +365,7 @@ restore_backup_files() {
else
log_warning "No .env file backup found"
fi
# Restore data directory
if [ -d "$BACKUP_DIR/data" ]; then
if [ -d "data" ]; then
@@ -380,24 +380,24 @@ restore_backup_files() {
else
log_warning "No data directory backup found"
fi
# Restore scripts directories
local scripts_dirs=("ct" "install" "tools" "vm")
for backup_name in "${scripts_dirs[@]}"; do
if [ -d "$BACKUP_DIR/$backup_name" ]; then
local target_dir="scripts/$backup_name"
log "Restoring $target_dir directory from backup..."
# Ensure scripts directory exists
if [ ! -d "scripts" ]; then
mkdir -p "scripts"
fi
# Remove existing directory if it exists
if [ -d "$target_dir" ]; then
rm -rf "$target_dir"
fi
if cp -r "$BACKUP_DIR/$backup_name" "$target_dir"; then
log_success "$target_dir directory restored from backup"
else
@@ -417,13 +417,7 @@ restore_backup_files() {
# Verify database was restored correctly
verify_database_restored() {
log "Verifying database was restored correctly..."
# Ensure data directory exists (will be auto-created by app if needed)
if [ ! -d "data" ]; then
log "Creating data directory..."
mkdir -p data
fi
# Check for both possible database filenames
local db_file=""
if [ -f "data/database.sqlite" ]; then
@@ -431,25 +425,23 @@ verify_database_restored() {
elif [ -f "data/settings.db" ]; then
db_file="data/settings.db"
else
# Database doesn't exist yet - this is OK for new installations
# The app will create it automatically via Prisma migrations
log_warning "No existing database file found - will be created automatically on first start"
return 0
log_error "Database file not found after restore! (checked database.sqlite and settings.db)"
return 1
fi
local db_size=$(stat -f%z "$db_file" 2>/dev/null || stat -c%s "$db_file" 2>/dev/null)
if [ "$db_size" -eq 0 ]; then
log_warning "Database file is empty - will be recreated by Prisma migrations"
return 0 # Don't fail the update, let Prisma recreate the database
return 0 # Don't fail the update, let Prisma recreate the database
fi
log_success "Database verified (file: $db_file, size: $db_size bytes)"
}
# Ensure DATABASE_URL is set in .env file for Prisma
ensure_database_url() {
log "Ensuring DATABASE_URL is set in .env file..."
# Check if .env file exists
if [ ! -f ".env" ]; then
log_warning ".env file not found, creating from .env.example..."
@@ -460,19 +452,19 @@ ensure_database_url() {
return 1
fi
fi
# Check if DATABASE_URL is already set
if grep -q "^DATABASE_URL=" .env; then
log "DATABASE_URL already exists in .env file"
return 0
fi
# Add DATABASE_URL to .env file
log "Adding DATABASE_URL to .env file..."
echo "" >>.env
echo "# Database" >>.env
echo "DATABASE_URL=\"file:./data/settings.db\"" >>.env
echo "" >> .env
echo "# Database" >> .env
echo "DATABASE_URL=\"file:./data/settings.db\"" >> .env
log_success "DATABASE_URL added to .env file"
}
@@ -489,9 +481,11 @@ check_service() {
fi
}
# Stop the application before updating
stop_application() {
# Change to the application directory if we're not already there
local app_dir
if [ -f "package.json" ] && [ -f "server.js" ]; then
@@ -509,9 +503,9 @@ stop_application() {
return 1
fi
fi
log "Working from application directory: $(pwd)"
# Check if systemd service is running and disable it temporarily
if check_service && systemctl is-active --quiet pvescriptslocal.service; then
log "Disabling systemd service temporarily to prevent auto-restart..."
@@ -524,7 +518,7 @@ stop_application() {
else
log "No running systemd service found"
fi
# Kill any remaining npm/node processes
log "Killing any remaining npm/node processes..."
local pids
@@ -543,9 +537,9 @@ stop_application() {
# 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"
@@ -561,48 +555,48 @@ update_files() {
"scripts/tools"
"scripts/vm"
)
# 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
# Verify critical files exist in source
if [ ! -f "$actual_source_dir/package.json" ]; then
log_error "package.json not found in source directory!"
return 1
fi
# Use process substitution instead of pipe to avoid subshell issues
local files_copied=0
local files_excluded=0
# 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"
find "$actual_source_dir" -type f > "$file_list"
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 ]] || [[ "$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
if ! cp "$file" "$rel_path"; then
log_error "Failed to copy $rel_path"
rm -f "$file_list"
@@ -612,47 +606,48 @@ update_files() {
else
files_excluded=$((files_excluded + 1))
fi
done <"$file_list"
done < "$file_list"
# Clean up temporary file
rm -f "$file_list"
# Verify critical files were copied
if [ ! -f "package.json" ]; then
log_error "package.json was not copied to target directory!"
return 1
fi
if [ ! -f "package-lock.json" ]; then
log_warning "package-lock.json was not copied!"
fi
log_success "Application files updated successfully ($files_copied files)"
}
# Install dependencies and build
install_and_build() {
log "Installing dependencies..."
# Verify package.json exists
if [ ! -f "package.json" ]; then
log_error "package.json not found! Cannot install dependencies."
return 1
fi
if [ ! -f "package-lock.json" ]; then
log_warning "No package-lock.json found, npm will generate one"
fi
# Create temporary file for npm output
local npm_log="/tmp/npm_install_$$.log"
# Ensure NODE_ENV is not set to production during install (we need devDependencies for build)
local old_node_env="${NODE_ENV:-}"
export NODE_ENV=development
# Run npm install to get ALL dependencies including devDependencies
if ! npm install --include=dev >"$npm_log" 2>&1; then
if ! npm install --include=dev > "$npm_log" 2>&1; then
log_error "Failed to install dependencies"
log_error "npm install output (last 30 lines):"
tail -30 "$npm_log" | while read -r line; do
@@ -661,20 +656,20 @@ install_and_build() {
rm -f "$npm_log"
return 1
fi
# Restore NODE_ENV
if [ -n "$old_node_env" ]; then
export NODE_ENV="$old_node_env"
else
unset NODE_ENV
fi
log_success "Dependencies installed successfully"
rm -f "$npm_log"
# Generate Prisma client
log "Generating Prisma client..."
if ! npx prisma generate >"$npm_log" 2>&1; then
if ! npx prisma generate > "$npm_log" 2>&1; then
log_error "Failed to generate Prisma client"
log_error "Prisma generate output:"
cat "$npm_log" | while read -r line; do
@@ -684,7 +679,7 @@ install_and_build() {
return 1
fi
log_success "Prisma client generated successfully"
# Check if Prisma migrations exist and are compatible
if [ -d "prisma/migrations" ]; then
log "Existing migration history detected"
@@ -693,10 +688,10 @@ install_and_build() {
else
log_warning "No existing migration history found - this may be a fresh install"
fi
# Run Prisma migrations
log "Running Prisma migrations..."
if ! npx prisma migrate deploy >"$npm_log" 2>&1; then
if ! npx prisma migrate deploy > "$npm_log" 2>&1; then
log_warning "Prisma migrations failed or no migrations to run"
log "Prisma migrate output:"
cat "$npm_log" | while read -r line; do
@@ -706,18 +701,15 @@ install_and_build() {
log_success "Prisma migrations completed successfully"
fi
rm -f "$npm_log"
log "Building application..."
# Set NODE_ENV to production for build
export NODE_ENV=production
# Unset TURBOPACK to prevent "Multiple bundler flags" error with --webpack
unset TURBOPACK 2>/dev/null || true
export TURBOPACK=''
# Create temporary file for npm build output
local build_log="/tmp/npm_build_$$.log"
if ! TURBOPACK='' npm run build >"$build_log" 2>&1; then
if ! npm run build > "$build_log" 2>&1; then
log_error "Failed to build application"
log_error "npm run build output:"
cat "$build_log" | while read -r line; do
@@ -726,18 +718,18 @@ install_and_build() {
rm -f "$build_log"
return 1
fi
# Log success and clean up
log_success "Application built successfully"
rm -f "$build_log"
log_success "Dependencies installed and application built successfully"
}
# Start the application after updating
start_application() {
log "Starting application..."
# Use the global variable to determine how to start
if [ "$SERVICE_WAS_RUNNING" = true ] && check_service; then
log "Service was running before update, re-enabling and starting systemd service..."
@@ -769,11 +761,11 @@ start_application() {
# Start application with npm
start_with_npm() {
log "Starting application with npm start..."
# Start in background
nohup npm start >server.log 2>&1 &
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
@@ -784,30 +776,13 @@ start_with_npm() {
fi
}
# Re-enable the systemd service on failure to prevent users from being locked out
re_enable_service_on_failure() {
if check_service; then
log "Re-enabling systemd service after failure..."
if systemctl enable pvescriptslocal.service 2>/dev/null; then
log_success "Service re-enabled"
if systemctl start pvescriptslocal.service 2>/dev/null; then
log_success "Service started"
else
log_warning "Failed to start service - manual intervention may be required"
fi
else
log_warning "Failed to re-enable service - manual intervention may be required"
fi
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..."
@@ -822,7 +797,7 @@ rollback() {
else
log_warning "No data directory backup found"
fi
# Restore .env file
if [ -f "$BACKUP_DIR/.env" ]; then
log "Restoring .env file..."
@@ -837,24 +812,24 @@ rollback() {
else
log_warning "No .env file backup found"
fi
# Restore scripts directories
local scripts_dirs=("ct" "install" "tools" "vm")
for backup_name in "${scripts_dirs[@]}"; do
if [ -d "$BACKUP_DIR/$backup_name" ]; then
local target_dir="scripts/$backup_name"
log "Restoring $target_dir directory from backup..."
# Ensure scripts directory exists
if [ ! -d "scripts" ]; then
mkdir -p "scripts"
fi
# Remove existing directory if it exists
if [ -d "$target_dir" ]; then
rm -rf "$target_dir"
fi
if mv "$BACKUP_DIR/$backup_name" "$target_dir"; then
log_success "$target_dir directory restored from backup"
else
@@ -864,17 +839,14 @@ rollback() {
log_warning "No $backup_name directory backup found"
fi
done
# Clean up backup directory
log "Cleaning up backup directory..."
rm -rf "$BACKUP_DIR"
else
log_error "No backup directory found for rollback"
fi
# Re-enable the service so users aren't locked out
re_enable_service_on_failure
log_error "Update failed. Please check the logs and try again."
exit 1
}
@@ -893,14 +865,14 @@ check_node_version() {
log "Detected Node.js version: $current"
if ((major_version == 24)); then
log_success "Node.js 24 already installed"
elif ((major_version < 24)); then
if (( major_version < 24 )); then
log_warning "Node.js < 24 detected → upgrading to Node.js 24 LTS..."
upgrade_node_to_24
else
elif (( major_version > 24 )); then
log_warning "Node.js > 24 detected → script tested only up to Node 24"
log "Continuing anyway…"
else
log_success "Node.js 24 already installed"
fi
}
@@ -908,39 +880,22 @@ check_node_version() {
upgrade_node_to_24() {
log "Preparing Node.js 24 upgrade…"
# Remove old nodesource repo files if they exist
# Remove old nodesource repo if it exists
if [ -f /etc/apt/sources.list.d/nodesource.list ]; then
log "Removing old nodesource.list file..."
rm -f /etc/apt/sources.list.d/nodesource.list
fi
if [ -f /etc/apt/sources.list.d/nodesource.sources ]; then
log "Removing old nodesource.sources file..."
rm -f /etc/apt/sources.list.d/nodesource.sources
fi
# Update apt cache first
log "Updating apt cache..."
apt-get update >>"$LOG_FILE" 2>&1 || true
# Install NodeSource repo for Node.js 24
log "Downloading Node.js 24 setup script..."
if ! curl -fsSL https://deb.nodesource.com/setup_24.x -o /tmp/node24_setup.sh; then
log_error "Failed to download Node.js 24 setup script"
re_enable_service_on_failure
exit 1
fi
if ! bash /tmp/node24_setup.sh >/tmp/node24_setup.log 2>&1; then
curl -fsSL https://deb.nodesource.com/setup_24.x -o /tmp/node24_setup.sh
if ! bash /tmp/node24_setup.sh > /tmp/node24_setup.log 2>&1; then
log_error "Failed to configure Node.js 24 repository"
tail -20 /tmp/node24_setup.log | while read -r line; do log_error "$line"; done
re_enable_service_on_failure
exit 1
fi
log "Installing Node.js 24…"
if ! apt-get install -y nodejs >>"$LOG_FILE" 2>&1; then
if ! apt-get install -y nodejs >> "$LOG_FILE" 2>&1; then
log_error "Failed to install Node.js 24"
re_enable_service_on_failure
exit 1
fi
@@ -957,21 +912,21 @@ main() {
init_log
log "Running as detached process"
sleep 3
else
init_log
fi
# 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"
bash "$0" --relocated
exit $?
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)"
@@ -988,76 +943,79 @@ main() {
exit 1
fi
fi
# Check dependencies
check_dependencies
# Load GitHub token for higher rate limits
load_github_token
# Check if service was running before update
if check_service && systemctl is-active --quiet pvescriptslocal.service; then
SERVICE_WAS_RUNNING=true
else
SERVICE_WAS_RUNNING=false
fi
# Get latest release info
local release_info
release_info=$(get_latest_release)
# Backup data directory
backup_data
# Stop the application before updating
stop_application
# Check Node.js version
check_node_version
#Update Node.js to 24
upgrade_node_to_24
# Download and extract release
local source_dir
source_dir=$(download_release "$release_info")
# Clear the original directory before updating
clear_original_directory
# Update files
if ! update_files "$source_dir"; then
log_error "File update failed, rolling back..."
rollback
fi
# Restore .env and data directory before building
restore_backup_files
# Verify database was restored correctly
if ! verify_database_restored; then
log_error "Database verification failed, rolling back..."
rollback
fi
# Ensure DATABASE_URL is set for Prisma
ensure_database_url
# Install dependencies and build
if ! install_and_build; then
log_error "Install and build failed, rolling back..."
rollback
fi
# Start the application
if ! start_application; then
log_error "Failed to start application after update"
rollback
fi
# Cleanup only after successful start
rm -rf "$source_dir"
rm -rf "/tmp/pve-update-$$"
rm -rf "$BACKUP_DIR"
log "Backup directory cleaned up"
log_success "Update completed successfully!"
}
@@ -1065,4 +1023,4 @@ main() {
if ! main "$@"; then
log_error "Update script failed with exit code $?"
exit 1
fi
fi