Compare commits

...
Author SHA1 Message Date
community-scripts-pr-app[bot]andgithub-actions[bot] ebf9cf656e Update CHANGELOG.md (#18502)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-06 18:32:26 +00:00
CanbiZ (MickLesk) c5c406b392 OPNsense VM: size the temp space check to the unpacked image | say why the EFI disk could not be allocated (#18404)
* OPNsense VM: size the temp space check to the unpacked image

The script refused to run unless TEMP_DIR had 20 GB free, a figure left
over from when the download went there too. The download now goes to the
image cache and only the unpacked FreeBSD image (2.5 GB) lands in
TEMP_DIR, so hosts with 8 to 16 GB free in /tmp stopped with exit 214.
Check the size xz reports for the image instead.

* OPNsense VM: say why the EFI disk could not be allocated

pvesm's error went to stderr only, so the two exit 220 runs in telemetry
carry no reason. As msg_error it reaches the log and the trace, e.g.
"disk image '.../vm-969-disk-0.qcow2' already exists" after an earlier
run left its disk on a storage that qm destroy does not clean.

* Clean up comments in opnsense-vm.sh

Removed comments about image cache and size calculation.
2026-11-06 19:31:58 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 745043bd9a Update CHANGELOG.md (#18490)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-06 07:58:59 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] c0df6c9870 Update CHANGELOG.md (#18486)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-05 20:22:30 +00:00
CanbiZ (MickLesk) 5e5e840458 fix(openwrt-vm): leave the NIC untagged on a bridge without a port (#18473)
Advanced mode prefills the LAN VLAN with 999. On a bridge with no
physical port that is not VLAN-aware (an internal vmbr1, the usual LAN
choice) pve-bridge cannot tag the tap, and qm start failed with "no
physical interface on bridge 'vmbr1'". The tag is dropped there with a
warning, for LAN and WAN alike.

The scripts base still pointed at DevScripts, where this script was
removed on 10-09, so every run was reported as a DevScripts run.
2026-11-05 21:21:58 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 4adef93eb9 Update CHANGELOG.md (#18485)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-05 20:21:25 +00:00
CanbiZ (MickLesk) 94aa8e7ab0 fix(podman): skip Portainer where Podman has no Quadlet (#18469)
Portainer and the agent are written as Quadlet .container units, which
need Podman 4.4. Debian 12 ships 4.3.1, so systemctl start portainer
failed with "Unit portainer.service not found". Without Quadlet both
prompts are skipped with a warning.
2026-11-05 21:20:55 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] eb7d180f9f Update CHANGELOG.md (#18484)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-05 20:18:12 +00:00
CanbiZ (MickLesk) 6e6ca04f24 truenas-VM: let a failed ISO lookup reach its own error (#18476)
* fix(truenas-vm): let a failed ISO lookup reach its own error

When download.truenas.com did not answer, the lookup handed an empty
path to an associative array (bad array subscript) and the pipeline
failed under pipefail, so the run ended with exit 1 at the SELECTED_ISO
line instead of the "Could not find a stable ISO" message. The index
fetch now fails on HTTP errors and retries, empty lines are skipped, and
the assignment no longer aborts on an empty result.

* fix(truenas-vm): use the engine's retry flags for the ISO index
2026-11-05 21:17:45 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] ce04c99aa2 Update CHANGELOG.md (#18482)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-05 16:23:05 +00:00
CanbiZ (MickLesk) df705aefc4 proxmox-mail-gateway: take the repo suite from the container (#18475)
* fix(proxmox-mail-gateway): take the repo suite from the container

The install hardcoded trixie, so a Debian 12 container got the PMG 9
repository and pmg-api could not resolve its trixie dependencies (held
broken packages). Suite and key now follow the container's codename;
bookworm still has PMG 8 with proxmox-mailgateway-container.

* fix(proxmox-mail-gateway): read the codename inline
2026-11-05 17:22:36 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 4fd7aa7edb Update CHANGELOG.md (#18481)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-05 15:32:06 +00:00
CanbiZ (MickLesk) c55556144a Immich: Bump to 3.3.6 and minor improvement (#18474)
* fix(immich): take the watchfiles wheel and retry GeoNames longer

The v3.3.5 lock pins watchfiles 0.21.0, which has no wheel for the
Python 3.13 machine-learning venv, so every ML install downloaded a Rust
toolchain and compiled it. Where static.rust-lang.org reset the
connection, uv sync failed three times. --upgrade-package watchfiles
takes 1.3.0, an abi3 wheel; only uvicorn's reload mode uses it.

download.geonames.org dropped connections on 11-03/04 and the first file
failed after three 10 s connects; those four calls now retry six times
with a 30 s connect timeout.

* Immich: bump release version to v3.3.6

v3.3.6 disables onnxruntime's telemetry in immich_ml, which runs from
source here, so nothing else changes. The lock still pins watchfiles
0.21.0, so the wheel upgrade stays.
2026-11-05 16:31:37 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 43481c25fd Update CHANGELOG.md (#18480)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-05 14:25:17 +00:00
CerberusStyle d2970719e4 Change npm start argument to --production (#18478)
* Change npm start argument to --production

* update service with --production

* Reload systemd daemon after updating service file
2026-11-05 15:24:44 +01:00
10 changed files with 61 additions and 31 deletions

No files matched your search

+19
View File
@@ -571,16 +571,35 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
</details>
## 2026-11-06
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- OPNsense VM: size the temp space check to the unpacked image | say why the EFI disk could not be allocated [@MickLesk](https://github.com/MickLesk) ([#18404](https://github.com/community-scripts/ProxmoxVE/pull/18404))
### 💾 Core
- fix: spot same-filesystem bind mounts on Alpine [@rcourtman](https://github.com/rcourtman) ([core#212](https://github.com/community-scripts/core/pull/212))
- fix: report a container that went down during setup, keep finished installs [@MickLesk](https://github.com/MickLesk) ([core#211](https://github.com/community-scripts/core/pull/211))
## 2026-11-05
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- OpenWRT-VM: leave the NIC untagged on a bridge without a port [@MickLesk](https://github.com/MickLesk) ([#18473](https://github.com/community-scripts/ProxmoxVE/pull/18473))
- Podman: skip Portainer where Podman has no Quadlet [@MickLesk](https://github.com/MickLesk) ([#18469](https://github.com/community-scripts/ProxmoxVE/pull/18469))
- Planka: change npm start argument to --production [@CerberusStyle](https://github.com/CerberusStyle) ([#18478](https://github.com/community-scripts/ProxmoxVE/pull/18478))
- OPNsense VM: wait for the FreeBSD login prompt instead of 180 seconds [@MickLesk](https://github.com/MickLesk) ([#18467](https://github.com/community-scripts/ProxmoxVE/pull/18467))
- #### 🔧 Refactor
- truenas-VM: let a failed ISO lookup reach its own error [@MickLesk](https://github.com/MickLesk) ([#18476](https://github.com/community-scripts/ProxmoxVE/pull/18476))
- proxmox-mail-gateway: take the repo suite from the container [@MickLesk](https://github.com/MickLesk) ([#18475](https://github.com/community-scripts/ProxmoxVE/pull/18475))
- Immich: Bump to 3.3.6 and minor improvement [@MickLesk](https://github.com/MickLesk) ([#18474](https://github.com/community-scripts/ProxmoxVE/pull/18474))
- Tududi: run 2.0 on PostgreSQL, keep SQLite installs on 1.15.0 [@MickLesk](https://github.com/MickLesk) ([#18465](https://github.com/community-scripts/ProxmoxVE/pull/18465))
### 💾 Core
+3 -3
View File
@@ -75,7 +75,7 @@ EOF
BASE_DIR=${STAGING_DIR}/base-images
SOURCE_DIR=${STAGING_DIR}/image-source
cd /tmp
RELEASE="v3.3.5"
RELEASE="v3.3.6"
if [[ -f ~/.intel_version ]]; then
INTEL_SRC="https://raw.githubusercontent.com/immich-app/immich/${RELEASE}/machine-learning"
curl -fsSL "${INTEL_SRC}/scripts/install-intel-runtime.sh" -o ./intel-runtime 2>/dev/null ||
@@ -292,7 +292,7 @@ EOF
msg_ok "Pre-installed Python ${ML_PYTHON}"
msg_info "Updating Intel OpenVINO machine-learning"
for attempt in $(seq 1 3); do
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT,UV_CONCURRENT_DOWNLOADS,UV_CONCURRENT_BUILDS,UV_CONCURRENT_INSTALLS -Pnu immich uv sync --extra openvino --no-dev --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT,UV_CONCURRENT_DOWNLOADS,UV_CONCURRENT_BUILDS,UV_CONCURRENT_INSTALLS -Pnu immich uv sync --extra openvino --no-dev --upgrade-package watchfiles --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
[[ $attempt -eq 3 ]] && { ML_FAILED=1; break; }
# Parallel fetches can trip DNS rate limits (AdGuard, Pi-hole); retry one at a time.
export UV_CONCURRENT_DOWNLOADS=1 UV_CONCURRENT_BUILDS=1 UV_CONCURRENT_INSTALLS=1
@@ -310,7 +310,7 @@ EOF
msg_ok "Pre-installed Python ${ML_PYTHON}"
msg_info "Updating machine-learning"
for attempt in $(seq 1 3); do
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT,UV_CONCURRENT_DOWNLOADS,UV_CONCURRENT_BUILDS,UV_CONCURRENT_INSTALLS -Pnu immich uv sync --extra cpu --no-dev --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT,UV_CONCURRENT_DOWNLOADS,UV_CONCURRENT_BUILDS,UV_CONCURRENT_INSTALLS -Pnu immich uv sync --extra cpu --no-dev --upgrade-package watchfiles --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
[[ $attempt -eq 3 ]] && { ML_FAILED=1; break; }
# Parallel fetches can trip DNS rate limits (AdGuard, Pi-hole); retry one at a time.
export UV_CONCURRENT_DOWNLOADS=1 UV_CONCURRENT_BUILDS=1 UV_CONCURRENT_INSTALLS=1
+2
View File
@@ -80,6 +80,8 @@ function update_script() {
msg_ok "Migrated Database"
msg_info "Starting Service"
sed -i 's|ExecStart=/usr/bin/npm start --prod$|ExecStart=/usr/bin/npm start --production|' /etc/systemd/system/planka.service
systemctl daemon-reload
systemctl start planka
msg_ok "Started Service"
msg_ok "Updated successfully!"
+7 -7
View File
@@ -23,7 +23,7 @@ if [[ "$(arch_resolve)" == "amd64" ]] && ! grep -qm1 sse4_2 /proc/cpuinfo; then
exit 236
fi
RELEASE="v3.3.5"
RELEASE="v3.3.6"
if lscpu | grep -q 'GenuineIntel'; then
echo ""
echo ""
@@ -146,10 +146,10 @@ mkdir -p {"${APP_DIR}","${UPLOAD_DIR}","${GEO_DIR}","${INSTALL_DIR}"/cache}
msg_info "Installing GeoNames data"
cd "$GEO_DIR"
curl_with_retry "https://download.geonames.org/export/dump/admin1CodesASCII.txt" "admin1CodesASCII.txt"
curl_with_retry "https://download.geonames.org/export/dump/admin2Codes.txt" "admin2Codes.txt"
curl_with_retry "https://download.geonames.org/export/dump/countryInfo.txt" "countryInfo.txt"
curl_with_retry "https://download.geonames.org/export/dump/cities500.zip" "cities500.zip"
CURL_RETRIES=6 CURL_CONNECT_TO=30 curl_with_retry "https://download.geonames.org/export/dump/admin1CodesASCII.txt" "admin1CodesASCII.txt"
CURL_RETRIES=6 CURL_CONNECT_TO=30 curl_with_retry "https://download.geonames.org/export/dump/admin2Codes.txt" "admin2Codes.txt"
CURL_RETRIES=6 CURL_CONNECT_TO=30 curl_with_retry "https://download.geonames.org/export/dump/countryInfo.txt" "countryInfo.txt"
CURL_RETRIES=6 CURL_CONNECT_TO=30 curl_with_retry "https://download.geonames.org/export/dump/cities500.zip" "cities500.zip"
curl_with_retry "https://raw.githubusercontent.com/nvkelso/natural-earth-vector/v5.1.2/geojson/ne_10m_admin_0_countries.geojson" "ne_10m_admin_0_countries.geojson"
unzip -q cities500.zip
date --iso-8601=seconds | tr -d "\n" >geodata-date.txt
@@ -463,7 +463,7 @@ if [[ -f ~/.openvino ]]; then
msg_ok "Pre-installed Python ${ML_PYTHON}"
msg_info "Installing Intel OpenVINO machine-learning"
for attempt in $(seq 1 3); do
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT,UV_CONCURRENT_DOWNLOADS,UV_CONCURRENT_BUILDS,UV_CONCURRENT_INSTALLS -Pnu immich uv sync --extra openvino --no-dev --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT,UV_CONCURRENT_DOWNLOADS,UV_CONCURRENT_BUILDS,UV_CONCURRENT_INSTALLS -Pnu immich uv sync --extra openvino --no-dev --upgrade-package watchfiles --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
[[ $attempt -eq 3 ]] && { msg_error "uv sync failed three times, the machine-learning environment was not built"; exit 1; }
# Parallel fetches can trip DNS rate limits (AdGuard, Pi-hole); retry one at a time.
export UV_CONCURRENT_DOWNLOADS=1 UV_CONCURRENT_BUILDS=1 UV_CONCURRENT_INSTALLS=1
@@ -481,7 +481,7 @@ else
msg_ok "Pre-installed Python ${ML_PYTHON}"
msg_info "Installing machine-learning"
for attempt in $(seq 1 3); do
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT,UV_CONCURRENT_DOWNLOADS,UV_CONCURRENT_BUILDS,UV_CONCURRENT_INSTALLS -Pnu immich uv sync --extra cpu --no-dev --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT,UV_CONCURRENT_DOWNLOADS,UV_CONCURRENT_BUILDS,UV_CONCURRENT_INSTALLS -Pnu immich uv sync --extra cpu --no-dev --upgrade-package watchfiles --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
[[ $attempt -eq 3 ]] && { msg_error "uv sync failed three times, the machine-learning environment was not built"; exit 1; }
# Parallel fetches can trip DNS rate limits (AdGuard, Pi-hole); retry one at a time.
export UV_CONCURRENT_DOWNLOADS=1 UV_CONCURRENT_BUILDS=1 UV_CONCURRENT_INSTALLS=1
+1 -1
View File
@@ -70,7 +70,7 @@ After=network.target
[Service]
WorkingDirectory=/opt/planka
ExecStart=/usr/bin/npm start --prod
ExecStart=/usr/bin/npm start --production
Restart=always
[Install]
+4
View File
@@ -48,6 +48,10 @@ msg_ok "Installed Podman"
mkdir -p /etc/containers/systemd
if [[ ! -x /usr/libexec/podman/quadlet ]]; then
msg_warn "Portainer runs as a Quadlet unit, which needs Podman 4.4 or newer (Debian 13); skipping it"
var_podman_portainer="n" var_podman_portainer_agent="n"
fi
prompt="${var_podman_portainer:-}"
[[ -n "$prompt" ]] || read -r -p "${TAB3}Would you like to add Portainer? <y/N> " prompt || true
if [[ ${prompt,,} =~ ^(y|yes)$ ]]; then
+2 -2
View File
@@ -16,9 +16,9 @@ update_os
msg_info "Installing Proxmox Mail Gateway"
setup_deb822_repo \
"pmg" \
"https://enterprise.proxmox.com/debian/proxmox-release-trixie.gpg" \
"https://enterprise.proxmox.com/debian/proxmox-release-$(get_os_info codename).gpg" \
"http://download.proxmox.com/debian/pmg" \
"trixie" \
"$(get_os_info codename)" \
"pmg-no-subscription"
$STD apt install -y proxmox-mailgateway-container
msg_ok "Installed Proxmox Mail Gateway"
+13 -1
View File
@@ -4,7 +4,7 @@
# License: MIT
# https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/community-scripts/DevScripts/main}"
COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main}"
source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/pve/vm-core.func")
load_functions
@@ -341,6 +341,18 @@ if [[ "$VM_STATE" != *stopped* ]]; then
fi
msg_ok "OpenWrt has shut down"
# pve-bridge tags a NIC on a port of the bridge; a bridge without one only takes tags when VLAN-aware
if [[ -n "$LAN_VLAN" && -d "/sys/class/net/${LAN_BRG}/bridge" && "$(cat "/sys/class/net/${LAN_BRG}/bridge/vlan_filtering" 2>/dev/null)" != "1" ]] &&
! ls "/sys/class/net/${LAN_BRG}/brif" 2>/dev/null | grep -qE '^((eth|bond)[0-9]+|en[^.]+)(\.[0-9]+)?$'; then
msg_warn "LAN bridge ${LAN_BRG} has no physical port and is not VLAN-aware; the LAN NIC stays untagged"
LAN_VLAN=""
fi
if [[ -n "$VLAN" && -d "/sys/class/net/${BRG}/bridge" && "$(cat "/sys/class/net/${BRG}/bridge/vlan_filtering" 2>/dev/null)" != "1" ]] &&
! ls "/sys/class/net/${BRG}/brif" 2>/dev/null | grep -qE '^((eth|bond)[0-9]+|en[^.]+)(\.[0-9]+)?$'; then
msg_warn "WAN bridge ${BRG} has no physical port and is not VLAN-aware; the WAN NIC stays untagged"
VLAN=""
fi
msg_info "Adding bridge interfaces on Proxmox side"
qm set $VMID \
-net0 virtio,bridge=${LAN_BRG},macaddr=${LAN_MAC}${LAN_VLAN}${MTU} \
+7 -15
View File
@@ -47,7 +47,7 @@ function check_disk_space() {
}
TEMP_DIR=$(mktemp -d)
if ! check_disk_space "$TEMP_DIR" 20 && [ -d "/var/tmp" ] && check_disk_space "/var/tmp" 20; then
if ! check_disk_space "$TEMP_DIR" 4 && [ -d "/var/tmp" ] && check_disk_space "/var/tmp" 4; then
rm -rf "$TEMP_DIR"
TEMP_DIR=$(mktemp -d /var/tmp/opnsense-vm.XXXXXX)
fi
@@ -364,15 +364,6 @@ FREEBSD_VER="$VM_INDEX_LATEST"
URL="$VM_INDEX_URL"
msg_ok "Download URL: ${CL}${BL}${URL}${CL}"
# Check available disk space (require at least 20GB for safety)
if ! check_disk_space "$TEMP_DIR" 20; then
AVAILABLE_GB=$(df -h "$TEMP_DIR" | awk 'NR==2 {print $4}')
msg_error "Insufficient disk space in temporary directory ($TEMP_DIR)."
msg_error "Available: ${AVAILABLE_GB}, Required: ~20GB for FreeBSD image decompression."
msg_error "Please free up space or ensure /tmp has sufficient storage."
exit 214
fi
msg_info "Downloading FreeBSD Image"
# A mirror serving an error page returns 200, so size decides whether this
# is an image. Anything real here is far above 5 MB.
@@ -381,11 +372,12 @@ vm_fetch_image "$URL" "$CACHE_FILE" --cache --verify-xz --min-bytes $((5 * 1024
echo -en "\e[1A\e[0K"
msg_ok "Downloaded ${CL}${BL}$(basename "$CACHE_FILE")${CL}"
# Check disk space again before decompression
if ! check_disk_space "$TEMP_DIR" 15; then
NEED_GB=$(($(xz --robot -l "$CACHE_FILE" | awk '$1 == "totals" {print $5}') / 1024 / 1024 / 1024 + 1))
if ! check_disk_space "$TEMP_DIR" "$NEED_GB"; then
AVAILABLE_GB=$(df -h "$TEMP_DIR" | awk 'NR==2 {print $4}')
msg_error "Insufficient disk space for decompression."
msg_error "Available: ${AVAILABLE_GB}, Required: ~15GB for decompressed image."
msg_error "Insufficient disk space for decompression in ${TEMP_DIR}."
msg_error "Available: ${AVAILABLE_GB}, Required: ~${NEED_GB}GB for the decompressed image."
msg_error "Please free up space or ensure /tmp or /var/tmp has sufficient storage."
exit 214
fi
@@ -417,7 +409,7 @@ while :; do
alloc_delay=$((alloc_delay * 2))
continue
fi
echo -e "$alloc_err" >&2
msg_error "Could not allocate the EFI disk on ${STORAGE}: ${alloc_err}"
exit 220
done
vm_import_disk "$VMID" "$FILE" "$STORAGE"
+3 -2
View File
@@ -39,7 +39,7 @@ function truenas_iso_lookup() {
local pre_releases=()
local all_paths=$(
curl -sL "$BASE_URL" |
curl -fsSL "${_CS_CURL_RETRY[@]}" --max-time 90 "$BASE_URL" |
grep -oE 'href="[^"]+\.iso"' |
sed 's/href="//; s/"$//' |
grep -vE '(MASTER|ALPHA)' |
@@ -47,6 +47,7 @@ function truenas_iso_lookup() {
)
while read -r path; do
[[ -n "$path" ]] || continue
local filename=$(basename "$path")
local version=$(echo "$filename" | sed -E 's/.*TrueNAS-SCALE-([0-9]{2}\.[0-9]{2}(\.[0-9]+)*(-RC[0-9]|-BETA[0-9])?)\.iso.*/\1/')
if [[ "$version" =~ (RC|BETA) ]]; then
@@ -139,7 +140,7 @@ post_to_api_vm
vm_select_storage "$HN"
if [ -z "${SELECTED_ISO:-}" ]; then
SELECTED_ISO=$(truenas_iso_lookup | grep -vE 'RC|BETA' | sort -V | tail -n 1)
SELECTED_ISO=$(truenas_iso_lookup | grep -vE 'RC|BETA' | sort -V | tail -n 1) || true
if [ -z "$SELECTED_ISO" ]; then
msg_error "Could not find a stable ISO for fallback."