Compare commits

..

12 Commits

Author SHA1 Message Date
CanbiZ (MickLesk)
c1eb00dabe fix(cluster): validate container IDs cluster-wide across all nodes
- Query /cluster/resources via pvesh to check all VMs/CTs on ALL nodes
- Check /etc/pve/nodes/*/qemu-server and /etc/pve/nodes/*/lxc dirs
- Handles pmxcfs sync delays that caused sporadic ID conflicts
- Remove duplicate validate_container_id/get_valid_container_id definitions
- Add max_attempts safeguard to prevent infinite loops
2026-02-14 14:02:40 +01:00
CanbiZ (MickLesk)
f762155870 feat(core): merge ProxmoxVED core.func with prompt utilities and unattended mode 2026-02-14 13:48:06 +01:00
community-scripts-pr-app[bot]
867d02d969 Update CHANGELOG.md (#11903)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-14 12:41:34 +00:00
CanbiZ (MickLesk)
1a9bbdd6e7 core: unified logging system with combined logs (#11761)
* refactor(logging): unified logging system with combined logs

- Add log_msg(), log_section(), strip_ansi() helper functions to core.func
- Extend msg_ok, msg_error, msg_warn, msg_info, msg_custom to write to log file
- Log container settings (default and advanced) to log file
- Combine host creation log and container installation log on failure
- Use app-specific log path: /tmp/{app}-{ctid}-{session}.log
- Add timestamps and section headers in log files
- Improve get_error_text() with combined log fallback chain
- Add ensure_log_on_host() for trap handlers to pull logs before API reporting

* linting
2026-02-14 13:41:09 +01:00
community-scripts-pr-app[bot]
3e0db150bd chore: update github-versions.json (#11902)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-14 12:08:49 +00:00
community-scripts-pr-app[bot]
6b3653627c Update CHANGELOG.md (#11899)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-14 10:39:34 +00:00
Copilot
733ad75dc1 Disable UniFi script - APT packages no longer available (#11898)
* Initial plan

* Add disabled flag and description to unifi.json

Co-authored-by: MickLesk <47820557+MickLesk@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: MickLesk <47820557+MickLesk@users.noreply.github.com>
2026-02-14 11:39:11 +01:00
community-scripts-pr-app[bot]
60aaaab3e7 chore: update github-versions.json (#11895)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-14 06:14:54 +00:00
community-scripts-pr-app[bot]
1f735cb31f Update CHANGELOG.md (#11893)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-14 00:22:13 +00:00
community-scripts-pr-app[bot]
adcbe8dae2 chore: update github-versions.json (#11892)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-14 00:21:47 +00:00
community-scripts-pr-app[bot]
bba520dbbf Update CHANGELOG.md (#11890)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-13 18:14:34 +00:00
community-scripts-pr-app[bot]
b43963d352 chore: update github-versions.json (#11889)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-13 18:14:07 +00:00
5 changed files with 661 additions and 99 deletions

View File

@@ -401,6 +401,18 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
</details>
## 2026-02-14
### 💾 Core
- #### ✨ New Features
- core: unified logging system with combined logs [@MickLesk](https://github.com/MickLesk) ([#11761](https://github.com/community-scripts/ProxmoxVE/pull/11761))
### ❔ Uncategorized
- Disable UniFi script - APT packages no longer available [@Copilot](https://github.com/Copilot) ([#11898](https://github.com/community-scripts/ProxmoxVE/pull/11898))
## 2026-02-13
### 🚀 Updated Scripts
@@ -414,8 +426,8 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
- #### 🔧 Refactor
- chore(donetick): add config entry for v0.1.73 [@tomfrenzel](https://github.com/tomfrenzel) ([#11872](https://github.com/community-scripts/ProxmoxVE/pull/11872))
- Refactor: Radicale [@vhsdream](https://github.com/vhsdream) ([#11850](https://github.com/community-scripts/ProxmoxVE/pull/11850))
- chore(donetick): add config entry for v0.1.73 [@tomfrenzel](https://github.com/tomfrenzel) ([#11872](https://github.com/community-scripts/ProxmoxVE/pull/11872))
### 💾 Core

View File

@@ -1,5 +1,5 @@
{
"generated": "2026-02-13T12:11:36Z",
"generated": "2026-02-14T12:08:41Z",
"versions": [
{
"slug": "2fauth",
@@ -67,9 +67,9 @@
{
"slug": "autobrr",
"repo": "autobrr/autobrr",
"version": "v1.72.1",
"version": "v1.73.0",
"pinned": false,
"date": "2026-01-30T12:57:58Z"
"date": "2026-02-13T16:37:28Z"
},
{
"slug": "autocaliweb",
@@ -200,9 +200,9 @@
{
"slug": "cloudreve",
"repo": "cloudreve/cloudreve",
"version": "4.13.0",
"version": "4.14.0",
"pinned": false,
"date": "2026-02-05T12:53:24Z"
"date": "2026-02-14T06:05:06Z"
},
{
"slug": "comfyui",
@@ -284,9 +284,9 @@
{
"slug": "domain-locker",
"repo": "Lissy93/domain-locker",
"version": "v0.1.3",
"version": "v0.1.4",
"pinned": false,
"date": "2026-02-11T10:03:32Z"
"date": "2026-02-14T07:41:29Z"
},
{
"slug": "domain-monitor",
@@ -354,9 +354,9 @@
{
"slug": "firefly",
"repo": "firefly-iii/firefly-iii",
"version": "v6.4.18",
"version": "v6.4.19",
"pinned": false,
"date": "2026-02-08T07:28:00Z"
"date": "2026-02-14T11:55:40Z"
},
{
"slug": "fladder",
@@ -445,9 +445,9 @@
{
"slug": "gotify",
"repo": "gotify/server",
"version": "v2.8.0",
"version": "v2.9.0",
"pinned": false,
"date": "2026-01-02T11:56:16Z"
"date": "2026-02-13T15:22:31Z"
},
{
"slug": "grist",
@@ -508,9 +508,9 @@
{
"slug": "homarr",
"repo": "homarr-labs/homarr",
"version": "v1.53.0",
"version": "v1.53.1",
"pinned": false,
"date": "2026-02-06T19:42:58Z"
"date": "2026-02-13T19:47:11Z"
},
{
"slug": "homebox",
@@ -578,9 +578,9 @@
{
"slug": "jackett",
"repo": "Jackett/Jackett",
"version": "v0.24.1103",
"version": "v0.24.1109",
"pinned": false,
"date": "2026-02-13T05:53:23Z"
"date": "2026-02-14T05:54:26Z"
},
{
"slug": "jellystat",
@@ -823,16 +823,16 @@
{
"slug": "metube",
"repo": "alexta69/metube",
"version": "2026.02.12",
"version": "2026.02.14",
"pinned": false,
"date": "2026-02-12T21:05:49Z"
"date": "2026-02-14T07:49:11Z"
},
{
"slug": "miniflux",
"repo": "miniflux/v2",
"version": "2.2.16",
"version": "2.2.17",
"pinned": false,
"date": "2026-01-07T03:26:27Z"
"date": "2026-02-13T20:30:17Z"
},
{
"slug": "monica",
@@ -1000,7 +1000,7 @@
"repo": "fosrl/pangolin",
"version": "1.15.4",
"pinned": false,
"date": "2026-02-13T00:54:02Z"
"date": "2026-02-13T23:01:29Z"
},
{
"slug": "paperless-ai",
@@ -1096,9 +1096,9 @@
{
"slug": "pocketbase",
"repo": "pocketbase/pocketbase",
"version": "v0.36.2",
"version": "v0.36.3",
"pinned": false,
"date": "2026-02-01T08:12:42Z"
"date": "2026-02-13T18:38:58Z"
},
{
"slug": "pocketid",
@@ -1313,9 +1313,9 @@
{
"slug": "semaphore",
"repo": "semaphoreui/semaphore",
"version": "v2.16.51",
"version": "v2.17.0",
"pinned": false,
"date": "2026-01-12T16:26:38Z"
"date": "2026-02-13T21:08:30Z"
},
{
"slug": "shelfmark",
@@ -1411,9 +1411,9 @@
{
"slug": "tandoor",
"repo": "TandoorRecipes/recipes",
"version": "2.5.0",
"version": "2.5.1",
"pinned": false,
"date": "2026-02-08T13:23:02Z"
"date": "2026-02-13T15:57:27Z"
},
{
"slug": "tasmoadmin",
@@ -1467,9 +1467,9 @@
{
"slug": "tianji",
"repo": "msgbyte/tianji",
"version": "v1.31.12",
"version": "v1.31.13",
"pinned": false,
"date": "2026-02-12T19:06:14Z"
"date": "2026-02-13T16:30:09Z"
},
{
"slug": "traccar",
@@ -1516,9 +1516,9 @@
{
"slug": "tududi",
"repo": "chrisvel/tududi",
"version": "v0.88.4",
"version": "v0.88.5",
"pinned": false,
"date": "2026-01-20T15:11:58Z"
"date": "2026-02-13T13:54:14Z"
},
{
"slug": "tunarr",
@@ -1565,9 +1565,9 @@
{
"slug": "uptimekuma",
"repo": "louislam/uptime-kuma",
"version": "2.1.0",
"version": "2.1.1",
"pinned": false,
"date": "2026-02-07T02:31:49Z"
"date": "2026-02-13T16:07:33Z"
},
{
"slug": "vaultwarden",

View File

@@ -14,6 +14,8 @@
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/ubiquiti-unifi.webp",
"config_path": "",
"description": "UniFi Network Server is a software that helps manage and monitor UniFi networks (Wi-Fi, Ethernet, etc.) by providing an intuitive user interface and advanced features. It allows network administrators to configure, monitor, and upgrade network devices, as well as view network statistics, client devices, and historical events. The aim of the application is to make the management of UniFi networks easier and more efficient.",
"disable": true,
"disable_description": "This script is disabled because UniFi no longer delivers APT packages for Debian systems. The installation relies on APT repositories that are no longer maintained or available. For more details, see: https://github.com/community-scripts/ProxmoxVE/issues/11876",
"install_methods": [
{
"type": "default",

View File

@@ -277,8 +277,9 @@ install_ssh_keys_into_ct() {
# ------------------------------------------------------------------------------
# validate_container_id()
#
# - Validates if a container ID is available for use
# - Checks if ID is already used by VM or LXC container
# - Validates if a container ID is available for use (CLUSTER-WIDE)
# - Checks cluster resources via pvesh for VMs/CTs on ALL nodes
# - Falls back to local config file check if pvesh unavailable
# - Checks if ID is used in LVM logical volumes
# - Returns 0 if ID is available, 1 if already in use
# ------------------------------------------------------------------------------
@@ -290,11 +291,35 @@ validate_container_id() {
return 1
fi
# Check if config file exists for VM or LXC
# CLUSTER-WIDE CHECK: Query all VMs/CTs across all nodes
# This catches IDs used on other nodes in the cluster
# NOTE: Works on single-node too - Proxmox always has internal cluster structure
# Falls back gracefully if pvesh unavailable or returns empty
if command -v pvesh &>/dev/null; then
local cluster_ids
cluster_ids=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null |
grep -oP '"vmid":\s*\K[0-9]+' 2>/dev/null || true)
if [[ -n "$cluster_ids" ]] && echo "$cluster_ids" | grep -qw "$ctid"; then
return 1
fi
fi
# LOCAL FALLBACK: Check if config file exists for VM or LXC
# This handles edge cases where pvesh might not return all info
if [[ -f "/etc/pve/qemu-server/${ctid}.conf" ]] || [[ -f "/etc/pve/lxc/${ctid}.conf" ]]; then
return 1
fi
# Check ALL nodes in cluster for config files (handles pmxcfs sync delays)
# NOTE: On single-node, /etc/pve/nodes/ contains just the one node - still works
if [[ -d "/etc/pve/nodes" ]]; then
for node_dir in /etc/pve/nodes/*/; do
if [[ -f "${node_dir}qemu-server/${ctid}.conf" ]] || [[ -f "${node_dir}lxc/${ctid}.conf" ]]; then
return 1
fi
done
fi
# Check if ID is used in LVM logical volumes
if lvs --noheadings -o lv_name 2>/dev/null | grep -qE "(^|[-_])${ctid}($|[-_])"; then
return 1
@@ -306,63 +331,30 @@ validate_container_id() {
# ------------------------------------------------------------------------------
# get_valid_container_id()
#
# - Returns a valid, unused container ID
# - Returns a valid, unused container ID (CLUSTER-AWARE)
# - Uses pvesh /cluster/nextid as starting point (already cluster-aware)
# - If provided ID is valid, returns it
# - Otherwise increments from suggested ID until a free one is found
# - Otherwise increments until a free one is found across entire cluster
# - Calls validate_container_id() to check availability
# ------------------------------------------------------------------------------
get_valid_container_id() {
local suggested_id="${1:-$(pvesh get /cluster/nextid)}"
while ! validate_container_id "$suggested_id"; do
suggested_id=$((suggested_id + 1))
done
echo "$suggested_id"
}
# ------------------------------------------------------------------------------
# validate_container_id()
#
# - Validates if a container ID is available for use
# - Checks if ID is already used by VM or LXC container
# - Checks if ID is used in LVM logical volumes
# - Returns 0 if ID is available, 1 if already in use
# ------------------------------------------------------------------------------
validate_container_id() {
local ctid="$1"
# Check if ID is numeric
if ! [[ "$ctid" =~ ^[0-9]+$ ]]; then
return 1
fi
# Check if config file exists for VM or LXC
if [[ -f "/etc/pve/qemu-server/${ctid}.conf" ]] || [[ -f "/etc/pve/lxc/${ctid}.conf" ]]; then
return 1
fi
# Check if ID is used in LVM logical volumes
if lvs --noheadings -o lv_name 2>/dev/null | grep -qE "(^|[-_])${ctid}($|[-_])"; then
return 1
fi
return 0
}
# ------------------------------------------------------------------------------
# get_valid_container_id()
#
# - Returns a valid, unused container ID
# - If provided ID is valid, returns it
# - Otherwise increments from suggested ID until a free one is found
# - Calls validate_container_id() to check availability
# ------------------------------------------------------------------------------
get_valid_container_id() {
local suggested_id="${1:-$(pvesh get /cluster/nextid)}"
local suggested_id="${1:-$(pvesh get /cluster/nextid 2>/dev/null || echo 100)}"
# Ensure we have a valid starting ID
if ! [[ "$suggested_id" =~ ^[0-9]+$ ]]; then
suggested_id=$(pvesh get /cluster/nextid 2>/dev/null || echo 100)
fi
local max_attempts=1000
local attempts=0
while ! validate_container_id "$suggested_id"; do
suggested_id=$((suggested_id + 1))
attempts=$((attempts + 1))
if [[ $attempts -ge $max_attempts ]]; then
msg_error "Could not find available container ID after $max_attempts attempts"
exit 1
fi
done
echo "$suggested_id"

View File

@@ -115,7 +115,7 @@ icons() {
BRIDGE="${TAB}🌉${TAB}${CL}"
NETWORK="${TAB}📡${TAB}${CL}"
GATEWAY="${TAB}🌐${TAB}${CL}"
DISABLEIPV6="${TAB}🚫${TAB}${CL}"
ICON_DISABLEIPV6="${TAB}🚫${TAB}${CL}"
DEFAULT="${TAB}⚙️${TAB}${CL}"
MACADDRESS="${TAB}🔗${TAB}${CL}"
VLANTAG="${TAB}🏷️${TAB}${CL}"
@@ -440,13 +440,13 @@ log_msg() {
local msg="$*"
local logfile
logfile="$(get_active_logfile)"
[[ -z "$msg" ]] && return
[[ -z "$logfile" ]] && return
# Ensure log directory exists
mkdir -p "$(dirname "$logfile")" 2>/dev/null || true
# Strip ANSI codes and write with timestamp
local clean_msg
clean_msg=$(strip_ansi "$msg")
@@ -464,10 +464,10 @@ log_section() {
local section="$1"
local logfile
logfile="$(get_active_logfile)"
[[ -z "$logfile" ]] && return
mkdir -p "$(dirname "$logfile")" 2>/dev/null || true
{
echo ""
echo "================================================================================"
@@ -551,7 +551,7 @@ spinner() {
local i=0
while true; do
local index=$((i++ % ${#chars[@]}))
printf "\r\033[2K%s %b" "${CS_YWB}${TAB}${chars[$index]}${TAB}${CS_CL}" "${CS_YWB}${msg}${CS_CL}"
printf "\r\033[2K%s %b" "${CS_YWB}${chars[$index]}${CS_CL}" "${CS_YWB}${msg}${CS_CL}"
sleep 0.1
done
}
@@ -665,7 +665,9 @@ msg_ok() {
clear_line
echo -e "$CM ${GN}${msg}${CL}"
log_msg "[OK] $msg"
unset MSG_INFO_SHOWN["$msg"]
local sanitized_msg
sanitized_msg=$(printf '%s' "$msg" | sed 's/\x1b\[[0-9;]*m//g; s/[^a-zA-Z0-9_]/_/g')
unset 'MSG_INFO_SHOWN['"$sanitized_msg"']' 2>/dev/null || true
}
# ------------------------------------------------------------------------------
@@ -878,6 +880,562 @@ is_verbose_mode() {
[[ "$verbose" != "no" || ! -t 2 ]]
}
# ------------------------------------------------------------------------------
# is_unattended()
#
# - Detects if script is running in unattended/non-interactive mode
# - Checks MODE variable first (primary method)
# - Falls back to legacy flags (PHS_SILENT, var_unattended)
# - Returns 0 (true) if unattended, 1 (false) otherwise
# - Used by prompt functions to auto-apply defaults
#
# Modes that are unattended:
# - default (1) : Use script defaults, no prompts
# - mydefaults (3) : Use user's default.vars, no prompts
# - appdefaults (4) : Use app-specific defaults, no prompts
#
# Modes that are interactive:
# - advanced (2) : Full wizard with all options
#
# Note: Even in advanced mode, install scripts run unattended because
# all values are already collected during the wizard phase.
# ------------------------------------------------------------------------------
is_unattended() {
# Primary: Check MODE variable (case-insensitive)
local mode="${MODE:-${mode:-}}"
mode="${mode,,}" # lowercase
case "$mode" in
default | 1)
return 0
;;
mydefaults | userdefaults | 3)
return 0
;;
appdefaults | 4)
return 0
;;
advanced | 2)
# Advanced mode is interactive ONLY during wizard
# Inside container (install scripts), it should be unattended
# Check if we're inside a container (no pveversion command)
if ! command -v pveversion &>/dev/null; then
# We're inside the container - all values already collected
return 0
fi
# On host during wizard - interactive
return 1
;;
esac
# Legacy fallbacks for compatibility
[[ "${PHS_SILENT:-0}" == "1" ]] && return 0
[[ "${var_unattended:-}" =~ ^(yes|true|1)$ ]] && return 0
[[ "${UNATTENDED:-}" =~ ^(yes|true|1)$ ]] && return 0
# No TTY available = unattended
[[ ! -t 0 ]] && return 0
# Default: interactive
return 1
}
# ------------------------------------------------------------------------------
# show_missing_values_warning()
#
# - Displays a summary of required values that used fallback defaults
# - Should be called at the end of install scripts
# - Only shows warning if MISSING_REQUIRED_VALUES array has entries
# - Provides clear guidance on what needs manual configuration
#
# Global:
# MISSING_REQUIRED_VALUES - Array of variable names that need configuration
#
# Example:
# # At end of install script:
# show_missing_values_warning
# ------------------------------------------------------------------------------
show_missing_values_warning() {
if [[ ${#MISSING_REQUIRED_VALUES[@]} -gt 0 ]]; then
echo ""
echo -e "${YW}╔════════════════════════════════════════════════════════════╗${CL}"
echo -e "${YW}║ ⚠️ MANUAL CONFIGURATION REQUIRED ║${CL}"
echo -e "${YW}╠════════════════════════════════════════════════════════════╣${CL}"
echo -e "${YW}║ The following values were not provided and need to be ║${CL}"
echo -e "${YW}║ configured manually for the service to work properly: ║${CL}"
echo -e "${YW}╟────────────────────────────────────────────────────────────╢${CL}"
for val in "${MISSING_REQUIRED_VALUES[@]}"; do
printf "${YW}${CL} • %-56s ${YW}${CL}\n" "$val"
done
echo -e "${YW}╟────────────────────────────────────────────────────────────╢${CL}"
echo -e "${YW}║ Check the service configuration files or environment ║${CL}"
echo -e "${YW}║ variables and update the placeholder values. ║${CL}"
echo -e "${YW}╚════════════════════════════════════════════════════════════╝${CL}"
echo ""
return 1
fi
return 0
}
# ------------------------------------------------------------------------------
# prompt_confirm()
#
# - Prompts user for yes/no confirmation with timeout and unattended support
# - In unattended mode: immediately returns default value
# - In interactive mode: waits for user input with configurable timeout
# - After timeout: auto-applies default value
#
# Arguments:
# $1 - Prompt message (required)
# $2 - Default value: "y" or "n" (optional, default: "n")
# $3 - Timeout in seconds (optional, default: 60)
#
# Returns:
# 0 - User confirmed (yes)
# 1 - User declined (no) or timeout with default "n"
#
# Example:
# if prompt_confirm "Proceed with installation?" "y" 30; then
# echo "Installing..."
# fi
#
# # Unattended: prompt_confirm will use default without waiting
# var_unattended=yes
# prompt_confirm "Delete files?" "n" && echo "Deleting" || echo "Skipped"
# ------------------------------------------------------------------------------
prompt_confirm() {
local message="${1:-Confirm?}"
local default="${2:-n}"
local timeout="${3:-60}"
local response
# Normalize default to lowercase
default="${default,,}"
[[ "$default" != "y" ]] && default="n"
# Build prompt hint
local hint
if [[ "$default" == "y" ]]; then
hint="[Y/n]"
else
hint="[y/N]"
fi
# Unattended mode: apply default immediately
if is_unattended; then
if [[ "$default" == "y" ]]; then
return 0
else
return 1
fi
fi
# Check if running in a TTY
if [[ ! -t 0 ]]; then
# Not a TTY, use default
if [[ "$default" == "y" ]]; then
return 0
else
return 1
fi
fi
# Interactive prompt with timeout
echo -en "${YW}${message} ${hint} (auto-${default} in ${timeout}s): ${CL}"
if read -t "$timeout" -r response; then
# User provided input
response="${response,,}" # lowercase
case "$response" in
y | yes)
return 0
;;
n | no)
return 1
;;
"")
# Empty response, use default
if [[ "$default" == "y" ]]; then
return 0
else
return 1
fi
;;
*)
# Invalid input, use default
echo -e "${YW}Invalid response, using default: ${default}${CL}"
if [[ "$default" == "y" ]]; then
return 0
else
return 1
fi
;;
esac
else
# Timeout occurred
echo "" # Newline after timeout
echo -e "${YW}Timeout - auto-selecting: ${default}${CL}"
if [[ "$default" == "y" ]]; then
return 0
else
return 1
fi
fi
}
# ------------------------------------------------------------------------------
# prompt_input()
#
# - Prompts user for text input with timeout and unattended support
# - In unattended mode: immediately returns default value
# - In interactive mode: waits for user input with configurable timeout
# - After timeout: auto-applies default value
#
# Arguments:
# $1 - Prompt message (required)
# $2 - Default value (optional, default: "")
# $3 - Timeout in seconds (optional, default: 60)
#
# Output:
# Prints the user input or default value to stdout
#
# Example:
# username=$(prompt_input "Enter username:" "admin" 30)
# echo "Using username: $username"
#
# # With validation
# while true; do
# port=$(prompt_input "Enter port:" "8080" 30)
# [[ "$port" =~ ^[0-9]+$ ]] && break
# echo "Invalid port number"
# done
# ------------------------------------------------------------------------------
prompt_input() {
local message="${1:-Enter value:}"
local default="${2:-}"
local timeout="${3:-60}"
local response
# Build display default hint
local hint=""
[[ -n "$default" ]] && hint=" (default: ${default})"
# Unattended mode: return default immediately
if is_unattended; then
echo "$default"
return 0
fi
# Check if running in a TTY
if [[ ! -t 0 ]]; then
# Not a TTY, use default
echo "$default"
return 0
fi
# Interactive prompt with timeout
echo -en "${YW}${message}${hint} (auto-default in ${timeout}s): ${CL}" >&2
if read -t "$timeout" -r response; then
# User provided input (or pressed Enter for empty)
if [[ -n "$response" ]]; then
echo "$response"
else
echo "$default"
fi
else
# Timeout occurred
echo "" >&2 # Newline after timeout
echo -e "${YW}Timeout - using default: ${default}${CL}" >&2
echo "$default"
fi
}
# ------------------------------------------------------------------------------
# prompt_input_required()
#
# - Prompts user for REQUIRED text input with fallback support
# - In unattended mode: Uses fallback value if no env var set (with warning)
# - In interactive mode: loops until user provides non-empty input
# - Tracks missing required values for end-of-script summary
#
# Arguments:
# $1 - Prompt message (required)
# $2 - Fallback/example value for unattended mode (optional)
# $3 - Timeout in seconds (optional, default: 120)
# $4 - Environment variable name hint for error messages (optional)
#
# Output:
# Prints the user input or fallback value to stdout
#
# Returns:
# 0 - Success (value provided or fallback used)
# 1 - Failed (interactive timeout without input)
#
# Global:
# MISSING_REQUIRED_VALUES - Array tracking fields that used fallbacks
#
# Example:
# # With fallback - script continues even in unattended mode
# token=$(prompt_input_required "Enter API Token:" "YOUR_TOKEN_HERE" 60 "var_api_token")
#
# # Check at end of script if any values need manual configuration
# if [[ ${#MISSING_REQUIRED_VALUES[@]} -gt 0 ]]; then
# msg_warn "Please configure: ${MISSING_REQUIRED_VALUES[*]}"
# fi
# ------------------------------------------------------------------------------
# Global array to track missing required values
declare -g -a MISSING_REQUIRED_VALUES=()
prompt_input_required() {
local message="${1:-Enter required value:}"
local fallback="${2:-CHANGE_ME}"
local timeout="${3:-120}"
local env_var_hint="${4:-}"
local response=""
# Check if value is already set via environment variable (if hint provided)
if [[ -n "$env_var_hint" ]]; then
local env_value="${!env_var_hint:-}"
if [[ -n "$env_value" ]]; then
echo "$env_value"
return 0
fi
fi
# Unattended mode: use fallback with warning
if is_unattended; then
if [[ -n "$env_var_hint" ]]; then
echo -e "${YW}⚠ Required value '${env_var_hint}' not set - using fallback: ${fallback}${CL}" >&2
MISSING_REQUIRED_VALUES+=("$env_var_hint")
else
echo -e "${YW}⚠ Required value not provided - using fallback: ${fallback}${CL}" >&2
MISSING_REQUIRED_VALUES+=("(unnamed)")
fi
echo "$fallback"
return 0
fi
# Check if running in a TTY
if [[ ! -t 0 ]]; then
echo -e "${YW}⚠ Not interactive - using fallback: ${fallback}${CL}" >&2
MISSING_REQUIRED_VALUES+=("${env_var_hint:-unnamed}")
echo "$fallback"
return 0
fi
# Interactive prompt - loop until non-empty input or use fallback on timeout
local attempts=0
while [[ -z "$response" ]]; do
attempts=$((attempts + 1))
if [[ $attempts -gt 3 ]]; then
echo -e "${YW}Too many empty inputs - using fallback: ${fallback}${CL}" >&2
MISSING_REQUIRED_VALUES+=("${env_var_hint:-manual_input}")
echo "$fallback"
return 0
fi
echo -en "${YW}${message} (required, timeout ${timeout}s): ${CL}" >&2
if read -t "$timeout" -r response; then
if [[ -z "$response" ]]; then
echo -e "${YW}This field is required. Please enter a value. (attempt ${attempts}/3)${CL}" >&2
fi
else
# Timeout occurred - use fallback
echo "" >&2
echo -e "${YW}Timeout - using fallback value: ${fallback}${CL}" >&2
MISSING_REQUIRED_VALUES+=("${env_var_hint:-timeout}")
echo "$fallback"
return 0
fi
done
echo "$response"
}
# ------------------------------------------------------------------------------
# prompt_select()
#
# - Prompts user to select from a list of options with timeout support
# - In unattended mode: immediately returns default selection
# - In interactive mode: displays numbered menu and waits for choice
# - After timeout: auto-applies default selection
#
# Arguments:
# $1 - Prompt message (required)
# $2 - Default option number, 1-based (optional, default: 1)
# $3 - Timeout in seconds (optional, default: 60)
# $4+ - Options to display (required, at least 2)
#
# Output:
# Prints the selected option value to stdout
#
# Returns:
# 0 - Success
# 1 - No options provided or invalid state
#
# Example:
# choice=$(prompt_select "Select database:" 1 30 "PostgreSQL" "MySQL" "SQLite")
# echo "Selected: $choice"
#
# # With array
# options=("Option A" "Option B" "Option C")
# selected=$(prompt_select "Choose:" 2 60 "${options[@]}")
# ------------------------------------------------------------------------------
prompt_select() {
local message="${1:-Select option:}"
local default="${2:-1}"
local timeout="${3:-60}"
shift 3
local options=("$@")
local num_options=${#options[@]}
# Validate options
if [[ $num_options -eq 0 ]]; then
echo "" >&2
return 1
fi
# Validate default
if [[ ! "$default" =~ ^[0-9]+$ ]] || [[ "$default" -lt 1 ]] || [[ "$default" -gt "$num_options" ]]; then
default=1
fi
# Unattended mode: return default immediately
if is_unattended; then
echo "${options[$((default - 1))]}"
return 0
fi
# Check if running in a TTY
if [[ ! -t 0 ]]; then
echo "${options[$((default - 1))]}"
return 0
fi
# Display menu
echo -e "${YW}${message}${CL}" >&2
local i
for i in "${!options[@]}"; do
local num=$((i + 1))
if [[ $num -eq $default ]]; then
echo -e " ${GN}${num})${CL} ${options[$i]} ${YW}(default)${CL}" >&2
else
echo -e " ${GN}${num})${CL} ${options[$i]}" >&2
fi
done
# Interactive prompt with timeout
echo -en "${YW}Select [1-${num_options}] (auto-select ${default} in ${timeout}s): ${CL}" >&2
local response
if read -t "$timeout" -r response; then
if [[ -z "$response" ]]; then
# Empty response, use default
echo "${options[$((default - 1))]}"
elif [[ "$response" =~ ^[0-9]+$ ]] && [[ "$response" -ge 1 ]] && [[ "$response" -le "$num_options" ]]; then
# Valid selection
echo "${options[$((response - 1))]}"
else
# Invalid input, use default
echo -e "${YW}Invalid selection, using default: ${options[$((default - 1))]}${CL}" >&2
echo "${options[$((default - 1))]}"
fi
else
# Timeout occurred
echo "" >&2 # Newline after timeout
echo -e "${YW}Timeout - auto-selecting: ${options[$((default - 1))]}${CL}" >&2
echo "${options[$((default - 1))]}"
fi
}
# ------------------------------------------------------------------------------
# prompt_password()
#
# - Prompts user for password input with hidden characters
# - In unattended mode: returns default or generates random password
# - Supports auto-generation of secure passwords
# - After timeout: generates random password if allowed
#
# Arguments:
# $1 - Prompt message (required)
# $2 - Default value or "generate" for auto-generation (optional)
# $3 - Timeout in seconds (optional, default: 60)
# $4 - Minimum length for validation (optional, default: 0 = no minimum)
#
# Output:
# Prints the password to stdout
#
# Example:
# password=$(prompt_password "Enter password:" "generate" 30 8)
# echo "Password set"
#
# # Require user input (no default)
# db_pass=$(prompt_password "Database password:" "" 60 12)
# ------------------------------------------------------------------------------
prompt_password() {
local message="${1:-Enter password:}"
local default="${2:-}"
local timeout="${3:-60}"
local min_length="${4:-0}"
local response
# Generate random password if requested
local generated=""
if [[ "$default" == "generate" ]]; then
generated=$(openssl rand -base64 16 2>/dev/null | tr -dc 'a-zA-Z0-9' | head -c 16)
[[ -z "$generated" ]] && generated=$(head /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 16)
default="$generated"
fi
# Unattended mode: return default immediately
if is_unattended; then
echo "$default"
return 0
fi
# Check if running in a TTY
if [[ ! -t 0 ]]; then
echo "$default"
return 0
fi
# Build hint
local hint=""
if [[ -n "$generated" ]]; then
hint=" (Enter for auto-generated)"
elif [[ -n "$default" ]]; then
hint=" (Enter for default)"
fi
[[ "$min_length" -gt 0 ]] && hint="${hint} [min ${min_length} chars]"
# Interactive prompt with timeout (silent input)
echo -en "${YW}${message}${hint} (timeout ${timeout}s): ${CL}" >&2
if read -t "$timeout" -rs response; then
echo "" >&2 # Newline after hidden input
if [[ -n "$response" ]]; then
# Validate minimum length
if [[ "$min_length" -gt 0 ]] && [[ ${#response} -lt "$min_length" ]]; then
echo -e "${YW}Password too short (min ${min_length}), using default${CL}" >&2
echo "$default"
else
echo "$response"
fi
else
echo "$default"
fi
else
# Timeout occurred
echo "" >&2 # Newline after timeout
echo -e "${YW}Timeout - using generated password${CL}" >&2
echo "$default"
fi
}
# ==============================================================================
# SECTION 6: CLEANUP & MAINTENANCE
# ==============================================================================
@@ -966,15 +1524,13 @@ check_or_create_swap() {
msg_error "No active swap detected"
read -p "Do you want to create a swap file? [y/N]: " create_swap
create_swap="${create_swap,,}" # to lowercase
if [[ "$create_swap" != "y" && "$create_swap" != "yes" ]]; then
if ! prompt_confirm "Do you want to create a swap file?" "n" 60; then
msg_info "Skipping swap file creation"
return 1
fi
read -p "Enter swap size in MB (e.g., 2048 for 2GB): " swap_size_mb
local swap_size_mb
swap_size_mb=$(prompt_input "Enter swap size in MB (e.g., 2048 for 2GB):" "2048" 60)
if ! [[ "$swap_size_mb" =~ ^[0-9]+$ ]]; then
msg_error "Invalid size input. Aborting."
return 1