From 98319ead8718eba752d1405ce5f741e9f4f6c4b4 Mon Sep 17 00:00:00 2001
From: Meliox <5264368+Meliox@users.noreply.github.com>
Date: Sat, 6 Sep 2025 23:12:39 +0200
Subject: [PATCH] Major script refactor (#120)
Adds a minor fix to #113
Refactors messages
Refactors many sed commands to heredoc making then much easier to maintain
---
pve-mod-gui-sensors.sh | 2360 ++++++++++++++++++++++------------------
1 file changed, 1276 insertions(+), 1084 deletions(-)
diff --git a/pve-mod-gui-sensors.sh b/pve-mod-gui-sensors.sh
index 73a9345..8b67e13 100644
--- a/pve-mod-gui-sensors.sh
+++ b/pve-mod-gui-sensors.sh
@@ -40,35 +40,40 @@ JSON_EXPORT_FILENAME="sensorsdata.json"
PVE_MANAGER_LIB_JS_FILE="/usr/share/pve-manager/js/pvemanagerlib.js"
NODES_PM_FILE="/usr/share/perl5/PVE/API2/Nodes.pm"
-# Helper functions
-function msg {
- echo -e "\e[0m$1\e[0m"
+#region message tools
+# Section header (bold)
+function msgb() {
+ local message="$1"
+ echo -e "\e[1m${message}\e[0m"
}
-#echo message in bold
-function msgb {
- echo -e "\e[1m$1\e[0m"
+# Info (green)
+function info() {
+ local message="$1"
+ echo -e "\e[0;32m[info] ${message}\e[0m"
}
-function info {
- echo -e "\e[0;32m[info] $1\e[0m"
+# Warning (yellow)
+function warn() {
+ local message="$1"
+ echo -e "\e[0;33m[warning] ${message}\e[0m"
}
-function warn {
- echo -e "\e[0;93m[warning] $1\e[0m"
+# Error (red)
+function err() {
+ local message="$1"
+ echo -e "\e[0;31m[error] ${message}\e[0m"
+ exit 1
}
-function err {
- echo -e "\e[0;31m[error] $1\e[0m"
- exit 1
+# Prompts (cyan or bold)
+function ask() {
+ local prompt="$1"
+ local response
+ read -p $'\n\e[1;36m'"${prompt}:"$'\e[0m ' response
+ echo "$response"
}
-
-function ask {
- read -p $'\n\e[0;32m'"$1:"$'\e[0m'" " response
- echo $response
-}
-
-# End of helper functions
+#endregion message tools
# Function to display usage information
function usage {
@@ -107,239 +112,300 @@ function install_packages {
}
function configure {
- SENSORS_DETECTED=false
- local sensorsOutput
+ SENSORS_DETECTED=false
+ local sensorsOutput
- if [ $DEBUG_REMOTE = true ]; then
- warn "Remote debugging is used. Sensor readings from dump file $DEBUG_JSON_FILE will be used."
- sensorsOutput=$(cat $DEBUG_JSON_FILE)
- else
- sensorsOutput=$(sensors -j 2>/dev/null | python3 -m json.tool)
- fi
+ # Load sensor data
+ if [ "$DEBUG_REMOTE" = true ]; then
+ warn "Remote debugging is used. Sensor readings from dump file $DEBUG_JSON_FILE will be used."
+ sensorsOutput=$(cat "$DEBUG_JSON_FILE")
+ else
+ sensorsOutput=$(sensors -j 2>/dev/null | python3 -m json.tool)
+ fi
- if [ $? -ne 0 ]; then
- err "Sensor output error.\n\nCommand output:\n${sensorsOutput}\n\nExiting...\n"
- fi
+ if [ $? -ne 0 ]; then
+ err "Sensor output error.\n\nCommand output:\n${sensorsOutput}\n\nExiting..."
+ fi
- # Check if CPU is part of known list for autoconfiguration
- msg "\nDetecting support for CPU temperature sensors..."
- supportedCPU=false
- for item in "${KNOWN_CPU_SENSORS[@]}"; do
- if (echo "$sensorsOutput" | grep -q "$item"); then
- echo $item
- supportedCPU=true
- fi
- done
+ #### CPU ####
+ msgb "\n=== Detecting CPU temperature sensors ==="
+ ENABLE_CPU=false
+ local cpuList=()
+ for item in "${KNOWN_CPU_SENSORS[@]}"; do
+ if echo "$sensorsOutput" | grep -q "$item"; then
+ cpuList+=("$item")
+ ENABLE_CPU=true
+ fi
+ done
- # Prompt user for which CPU temperature to use
- if [ $supportedCPU = true ]; then
- while true; do
- local choiceTempDisplayType=$(ask "Do you wish to display temperatures for all cores [C] or just an average temperature per CPU [a] (note: AMD only supports average)? (C/a)")
- case "$choiceTempDisplayType" in
- # Set temperature search criteria
- [cC] | "")
- CPU_TEMP_TARGET="Core"
- info "Temperatures will be displayed for all cores."
- ;;
- [aA])
- CPU_TEMP_TARGET="Package"
- info "An average temperature will be displayed per CPU."
- ;;
- *)
- # If the user enters an invalid input, print an warning message and retry as>
- warn "Invalid input."
- continue
- ;;
- esac
- break
- done
- SENSORS_DETECTED=true
- else
- warn "No CPU temperature sensors found."
- fi
+ if [ "$ENABLE_CPU" = true ]; then
+ info "Detected CPU sensors (${#cpuList[@]}): $(IFS=,; echo "${cpuList[*]}")"
+ SENSORS_DETECTED=true
+ while true; do
+ local choice=$(ask "Display temperatures for all cores [C] or average per CPU [a] (AMD only supports average)? (C/a)")
+ case "$choice" in
+ [cC]|"")
+ CPU_TEMP_TARGET="Core"
+ info "Temperatures will be displayed for all cores."
+ break
+ ;;
+ [aA])
+ CPU_TEMP_TARGET="Package"
+ info "An average temperature will be displayed per CPU."
+ break
+ ;;
+ *)
+ warn "Invalid input, please choose C or a."
+ ;;
+ esac
+ done
+ else
+ warn "No CPU temperature sensors found."
+ fi
- # Look for ram temps
- msg "\nDetecting support for RAM temperature sensors..."
- if (echo "$sensorsOutput" | grep -q '"SODIMM":'); then
- msg "Detected RAM temperature sensors:\n$(echo "$sensorsOutput" | grep -o '"SODIMM[^"]*"' | sed 's/"//g')"
- ENABLE_RAM_TEMP=true
- SENSORS_DETECTED=true
- else
- warn "No RAM temperature sensors found."
- ENABLE_RAM_TEMP=false
- fi
+ #### RAM ####
+ msgb "\n=== Detecting RAM temperature sensors ==="
+ local ramList=($(echo "$sensorsOutput" | grep -o '"SODIMM[^"]*"' | sed 's/"//g'))
+ if [ ${#ramList[@]} -gt 0 ]; then
+ info "Detected RAM sensors (${#ramList[@]}): $(IFS=,; echo "${ramList[*]}")"
+ ENABLE_RAM_TEMP=true
+ SENSORS_DETECTED=true
+ else
+ warn "No RAM temperature sensors found."
+ ENABLE_RAM_TEMP=false
+ fi
- # Check if HDD/SSD data is installed
- msg "\nDetecting support for HDD/SDD temperature sensors..."
- if (lsmod | grep -wq "drivetemp"); then
- # Check if SDD/HDD data is available
- if (echo "$sensorsOutput" | grep -q "drivetemp-scsi-"); then
- msg "Detected sensors:\n$(echo "$sensorsOutput" | grep -o '"drivetemp-scsi[^"]*"' | sed 's/"//g')"
- ENABLE_HDD_TEMP=true
- SENSORS_DETECTED=true
- else
- warn "Kernel module \"drivetemp\" is not installed. HDD/SDD temperatures will not be available."
- ENABLE_HDD_TEMP=false
- fi
- else
- warn "No HDD/SSD temperature sensors found."
- ENABLE_HDD_TEMP=false
- fi
+ #### HDD/SSD ####
+ msgb "\n=== Detecting HDD/SSD temperature sensors ==="
+ local hddList=($(echo "$sensorsOutput" | grep -o '"drivetemp-scsi[^"]*"' | sed 's/"//g'))
+ if [ ${#hddList[@]} -gt 0 ]; then
+ info "Detected HDD/SSD sensors (${#hddList[@]}): $(IFS=,; echo "${hddList[*]}")"
+ ENABLE_HDD_TEMP=true
+ SENSORS_DETECTED=true
+ else
+ warn "No HDD/SSD temperature sensors found."
+ ENABLE_HDD_TEMP=false
+ fi
- # Check if NVMe data is available
- msg "\nDetecting support for NVMe temperature sensors..."
- if (echo "$sensorsOutput" | grep -q "nvme-"); then
- msg "Detected sensors:\n$(echo "$sensorsOutput" | grep -o '"nvme[^"]*"' | sed 's/"//g')"
- ENABLE_NVME_TEMP=true
- SENSORS_DETECTED=true
- else
- warn "No NVMe temperature sensors found."
- ENABLE_NVME_TEMP=false
- fi
+ #### NVMe ####
+ msgb "\n=== Detecting NVMe temperature sensors ==="
+ local nvmeList=($(echo "$sensorsOutput" | grep -o '"nvme[^"]*"' | sed 's/"//g'))
+ if [ ${#nvmeList[@]} -gt 0 ]; then
+ info "Detected NVMe sensors (${#nvmeList[@]}): $(IFS=,; echo "${nvmeList[*]}")"
+ ENABLE_NVME_TEMP=true
+ SENSORS_DETECTED=true
+ else
+ warn "No NVMe temperature sensors found."
+ ENABLE_NVME_TEMP=false
+ fi
- # Look for fan speeds
- msg "\nDetecting support for fan speed readings..."
- if (echo "$sensorsOutput" | grep -q "fan[0-9]*_input"); then
- msg "Detected fan speed sensors:\n$(echo $sensorsOutput | grep -Po '"[^"]*":\s*\{\s*"fan[0-9]*_input[^}]*' | sed -E 's/"([^"]*)":.*/\1/')"
- ENABLE_FAN_SPEED=true
- SENSORS_DETECTED=true
- # Prompt user for display zero speed fans
- local choiceDisplayZeroSpeedFans=$(ask "Do you wish to display fans reporting a speed of zero? If no, only active fans will be displayed. (Y/n)")
- case "$choiceDisplayZeroSpeedFans" in
- # Set temperature search criteria
- [yY]|"")
- DISPLAY_ZERO_SPEED_FANS=true
- ;;
- [nN] )
- DISPLAY_ZERO_SPEED_FANS=false
- ;;
- *)
- # If the user enters an invalid input, print an error message and exit the script with a non-zero status code
- err "Invalid input. Exiting..."
- ;;
- esac
- else
- warn "No fan speed sensors found."
- ENABLE_FAN_SPEED=false
- fi
+ #### Fans ####
+ msgb "\n=== Detecting fan speed sensors ==="
+ local fanList=$(echo "$sensorsOutput" | grep -B 1 '"fan[0-9]*_input"' | grep -Po '"[^"]*":\s*\{$' | sed 's/"//g' | sed 's/: {//' | paste -sd ',' -)
+ local fanCount=$(echo "$sensorsOutput" | grep -c '"fan[0-9]*_input"')
+ if [ ${#fanList[@]} -gt 0 ]; then
+ info "Detected fan speed sensors ($fanCount): $fanList"
+ ENABLE_FAN_SPEED=true
+ SENSORS_DETECTED=true
- if [ $SENSORS_DETECTED = true ]; then
- local choiceTempUnit=$(ask "Do you wish to display temperatures in degrees Celsius [C] or Fahrenheit [f]? (C/f)")
- case "$choiceTempUnit" in
- [cC] | "")
- TEMP_UNIT="C"
- info "Temperatures will be presented in degrees Celsius."
- ;;
- [fF])
- TEMP_UNIT="F"
- info "Temperatures will be presented in degrees Fahrenheit."
- ;;
- *)
- warn "Invalid unit selected. Temperatures will be displayed in degrees Celsius."
- TEMP_UNIT="C"
- ;;
- esac
- fi
+ local choice=$(ask "Display fans reporting zero speed? (Y/n)")
+ case "$choice" in
+ [yY]|"")
+ DISPLAY_ZERO_SPEED_FANS=true
+ info "Zero-speed fans will be displayed."
+ ;;
+ [nN])
+ DISPLAY_ZERO_SPEED_FANS=false
+ info "Only active fans will be displayed."
+ ;;
+ *)
+ warn "Invalid input. Defaulting to show zero-speed fans."
+ DISPLAY_ZERO_SPEED_FANS=true
+ ;;
+ esac
+ else
+ warn "No fan speed sensors found."
+ ENABLE_FAN_SPEED=false
+ fi
- # Prompt user for enabling UPS
- local choiseEnableUPS=$(ask "Do you wish to enable information from an attached UPS (requires configured UPS server and installed UPS client from Network UPS Tools already configured beforehand). (Y/n)")
- case "$choiseEnableUPS" in
- [yY] | "")
- # Test the connection using upsc command
- if [ $DEBUG_REMOTE = true ]; then
- upsOutput=$(cat $DEBUG_UPS_FILE)
- echo "Remote debugging is used. UPS readings from dump file $DEBUG_UPS_FILE will be used."
- upsConnection="DEBUG_UPS"
- else
- # Prompt user for UPS connection details
- upsConnection=$(ask "Enter connection details for the UPS (e.g., upsname[@hostname[:port]])")
+ #### Temperature Units ####
+ if [ "$SENSORS_DETECTED" = true ]; then
+ local unit=$(ask "Display temperatures in Celsius [C] or Fahrenheit [f]? (C/f)")
+ case "$unit" in
+ [cC]|"")
+ TEMP_UNIT="C"
+ info "Using Celsius."
+ ;;
+ [fF])
+ TEMP_UNIT="F"
+ info "Using Fahrenheit."
+ ;;
+ *)
+ warn "Invalid selection. Defaulting to Celsius."
+ TEMP_UNIT="C"
+ ;;
+ esac
+ fi
- if (! command -v upsc &>/dev/null); then
- err "The 'upsc' command is not available. Please install the 'nut-client' package and ensure it is configured correctly. Exiting..."
- fi
+ #### UPS ####
+ local choiceUPS=$(ask "Enable UPS information? (Y/n)")
+ case "$choiceUPS" in
+ [yY]|"")
+ if [ "$DEBUG_REMOTE" = true ]; then
+ upsOutput=$(cat "$DEBUG_UPS_FILE")
+ echo "Remote debugging: UPS readings from $DEBUG_UPS_FILE"
+ upsConnection="DEBUG_UPS"
+ else
+ upsConnection=$(ask "Enter UPS connection (e.g., upsname[@hostname[:port]])")
+ if ! command -v upsc &>/dev/null; then
+ err "The 'upsc' command is not available. Install 'nut-client'."
+ fi
+ upsOutput=$(upsc "$upsConnection" 2>&1)
+ fi
- upsOutput=$(upsc "$upsConnection" 2>&1)
- fi
+ if echo "$upsOutput" | grep -q "device.model:"; then
+ modelName=$(echo "$upsOutput" | grep "device.model:" | cut -d':' -f2- | xargs)
+ ENABLE_UPS=true
+ info "Connected to UPS model: $modelName at $upsConnection."
+ else
+ warn "Failed to connect to UPS at '$upsConnection'."
+ ENABLE_UPS=false
+ fi
+ ;;
+ [nN])
+ ENABLE_UPS=false
+ info "UPS information will NOT be displayed."
+ ;;
+ *)
+ warn "Invalid selection. UPS info will NOT be displayed."
+ ENABLE_UPS=false
+ ;;
+ esac
- # Check for device.model in the output to confirm successful connection
- if (echo "$upsOutput" | grep -q "device.model:"); then
- # Extract the model name
- modelName=$(echo "$upsOutput" | grep "device.model:" | cut -d':' -f2- | xargs)
- ENABLE_UPS=true
- echo "Successfully connected to UPS model: $modelName at $upsConnection."
- info "UPS information will be displayed..."
- else
- warn "Failed to connect to UPS at '$upsConnection'. No valid UPS model found."
- warn "Error: $upsOutput"
- ENABLE_UPS=false
- fi
+ #### System Info ####
+ msgb "\n=== System Information ==="
+ for i in 1 2; do
+ echo "type ${i})"
+ dmidecode -t "$i" | awk -F': ' '/Manufacturer|Product Name|Serial Number/ {print $1": "$2}'
+ done
+ local choiceSysInfo=$(ask "Enable system information? (1/2/n)")
+ case "$choiceSysInfo" in
+ [1]|"")
+ ENABLE_SYSTEM_INFO=true
+ SYSTEM_INFO_TYPE=1
+ info "System information will be displayed."
+ ;;
+ [2])
+ ENABLE_SYSTEM_INFO=true
+ SYSTEM_INFO_TYPE=2
+ info "Motherboard information will be displayed."
+ ;;
+ [nN])
+ ENABLE_SYSTEM_INFO=false
+ info "System information will NOT be displayed."
+ ;;
+ *)
+ warn "Invalid selection. Defaulting to system information."
+ ENABLE_SYSTEM_INFO=true
+ SYSTEM_INFO_TYPE=1
+ ;;
+ esac
- ;;
- [nN])
- ENABLE_UPS=false
- info "UPS information will NOT be displayed..."
- ;;
- *)
- warn "Invalid selection. UPS information will not be displayed."
- ENABLE_UPS=false
- ;;
- esac
- echo ""
-
- # DMI Type:
- # 1 ... System Information
- # 2 ... Base Board Information (for self-made PC)
- for i in 1 2; do
- echo "type ${i})"
- dmidecode -t ${i} | awk -F': ' '/Manufacturer|Product Name|Serial Number/ {print $1": "$2}'
- done
- local choiceEnableSystemInfo=$(ask "Do you wish to enable system information? (1/2/n)")
- case "$choiceEnableSystemInfo" in
- [1] | "")
- ENABLE_SYSTEM_INFO=true
- SYSTEM_INFO_TYPE=1
- info "System information will be displayed..."
- ;;
- [2])
- ENABLE_SYSTEM_INFO=true
- SYSTEM_INFO_TYPE=2
- info "Motherboard information will be displayed..."
- ;;
- [nN])
- ENABLE_SYSTEM_INFO=false
- info "System information will NOT be displayed..."
- ;;
- *)
- warn "Invalid selection. System information will be displayed."
- ENABLE_SYSTEM_INFO=true
- ;;
- esac
- echo # add a new line
+ #### Final Check ####
+ if [ "$SENSORS_DETECTED" = false ] && [ "$ENABLE_UPS" = false ] && [ "$ENABLE_SYSTEM_INFO" = false ]; then
+ err "No sensors detected, UPS or system info enabled. Exiting."
+ fi
}
+
# Function to install the modification
function install_mod {
- check_root_privileges
+ msgb "\n== Preparing mod installation =="
+ check_root_privileges
+ check_mod_installation
+ configure
+ perform_backup
- if [[ -n $(cat $NODES_PM_FILE | grep -e "$res->{sensorsOutput}") ]] && [[ -n $(cat $NODES_PM_FILE | grep -e "$res->{systemInfo}") ]]; then
- err "Mod is already installed. Uninstall existing before installing."
- fi
+ #### Insert information retrieval code ####
+ msgb "\n=== Inserting information retrieval code ==="
+ insert_node_info
- msg "\nPreparing mod installation..."
- configure
- perform_backup
+ #### Temperature helper parameters ####
+ msgb "\n=== Creating temperature conversion helper ==="
+ HELPERCTORPARAMS=$([[ "$TEMP_UNIT" = "F" ]] && \
+ echo '{srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: PVE.mod.TempHelper.FAHRENHEIT}' || \
+ echo '{srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: PVE.mod.TempHelper.CELSIUS}')
+ info "Temperature helper configured for $TEMP_UNIT."
- if [ $SENSORS_DETECTED = true ]; then
- local sensorsCmd
- if [ $DEBUG_REMOTE = true ]; then
- sensorsCmd="cat \"$DEBUG_JSON_FILE\""
- else
- # WTF: sensors -f used for Fahrenheit breaks the fan speeds :|
- #local sensorsCmd=$([[ "$TEMP_UNIT" = "F" ]] && echo "sensors -j -f" || echo "sensors -j")
- sensorsCmd="sensors -j 2>/dev/null | python3 -m json.tool"
- fi
- # Insert sensor data collection and JSON sanitization before the disk info line
- sed -i '/my \$dinfo = df('\''\/'\'', 1);/i\
+ #### Expand StatusView space ####
+ expand_statusview_space
+
+ #### Insert temperature helper ####
+ generate_and_insert_temp_helper
+
+ #### Generate and insert widgets ####
+ msgb "\n=== Generating and inserting widgets ==="
+
+ generate_and_insert_widget "$ENABLE_SYSTEM_INFO" "generate_system_info" "system_info"
+ generate_and_insert_widget "$ENABLE_UPS" "generate_ups_widget" "ups"
+ generate_and_insert_widget "$ENABLE_HDD_TEMP" "generate_hdd_widget" "hdd"
+ generate_and_insert_widget "$ENABLE_NVME_TEMP" "generate_nvme_widget" "nvme"
+
+ if [[ "$ENABLE_HDD_TEMP" = true || "$ENABLE_NVME_TEMP" = true ]]; then
+ generate_drive_header
+ info "Drive headers added."
+ fi
+
+ generate_and_insert_widget "$ENABLE_FAN_SPEED" "generate_fan_widget" "fan"
+ generate_and_insert_widget "$ENABLE_RAM_TEMP" "generate_ram_widget" "ram"
+ generate_and_insert_widget "$ENABLE_CPU" "generate_cpu_widget" "cpu"
+
+ #### Visual separation ####
+ add_visual_separator
+ info "Added visual separator for modified items."
+
+ #### Node summary ####
+ setup_node_summary_container
+ info "Node summary box moved into its own container."
+
+ msgb "\n=== Finalizing installation ==="
+ msg "Sensor display items added to the summary panel in \"$PVE_MANAGER_LIB_JS_FILE\"."
+
+ restart_proxy
+ msg "Installation completed."
+ info "Clear the browser cache to ensure all changes are visualized."
+}
+
+
+#region node info insertion
+# Main insertion routine
+insert_node_info() {
+ local output_file="$NODES_PM_FILE"
+
+ collect_sensors_output "$output_file"
+
+ if [[ $ENABLE_UPS == true ]]; then
+ collect_ups_output "$output_file"
+ fi
+
+ if [[ $ENABLE_SYSTEM_INFO == true ]]; then
+ collect_system_info "$output_file"
+ fi
+}
+
+# Collect lm-sensors data
+collect_sensors_output() {
+ local output_file="$1"
+ local sensorsCmd
+
+ if [[ $DEBUG_REMOTE == true ]]; then
+ sensorsCmd="cat \"$DEBUG_JSON_FILE\""
+ else
+ # Note: sensors -f (Fahrenheit) breaks fan speeds
+ sensorsCmd="sensors -j 2>/dev/null | python3 -m json.tool"
+ fi
+ #region sensors heredoc
+ sed -i '/my \$dinfo = df('\''\/'\'', 1);/i\
\
# Collect sensor data from lm-sensors\
$res->{sensorsOutput} = `'"$sensorsCmd"'`;\
@@ -359,882 +425,995 @@ function install_mod {
# This prevents JSON key overwrites when multiple SODIMM sensors exist\
# Example: "SODIMM":{"temp3_input":34.0} becomes "SODIMM3":{"temp3_input":34.0}\
$res->{sensorsOutput} =~ s/\\"SODIMM\\":\\{\\"temp(\\d+)_input\\"/\\"SODIMM$1\\":\\{\\"temp$1_input\\"/g;\
- ' "$NODES_PM_FILE"
- msg "Sensors' output added to \"$NODES_PM_FILE\"."
- fi
+ ' "$NODES_PM_FILE"
+ #endregion sensors heredoc
+ info "Sensors' retriever added to \"$output_file\"."
+}
- if [ $ENABLE_UPS = true ]; then
- local upsCmd
- if [ $DEBUG_REMOTE = true ]; then
- upsCmd="cat \"$DEBUG_UPS_FILE\""
- else
- upsCmd="upsc \"$upsConnection\" 2>/dev/null"
- fi
+# Collect UPS data
+collect_ups_output() {
+ local output_file="$1"
+ local ups_cmd
- # Insert UPS data collection before the disk info line
- sed -i "/my \$dinfo = df('\/', 1);/i\\
+ if [[ $DEBUG_REMOTE == true ]]; then
+ ups_cmd="cat \"$DEBUG_UPS_FILE\""
+ else
+ ups_cmd="upsc \"$upsConnection\" 2>/dev/null"
+ fi
+ #region ups heredoc
+ sed -i "/my \$dinfo = df('\/', 1);/i\\
\\
# Collect UPS status information\\
- \$res->{upsc} = \\\`$upsCmd\\\`;\\
- " "$NODES_PM_FILE"
-
- msg "UPS output added to \"$NODES_PM_FILE\"."
- fi
+ \$res->{upsc} = \\\`$ups_cmd\\\`;\\
+ " "$NODES_PM_FILE"
+ #endregion ups heredoc
+ info "UPS retriever added to \"$output_file\"."
+}
- if [ $ENABLE_SYSTEM_INFO = true ]; then
- local systemInfoCmd=$(dmidecode -t ${SYSTEM_INFO_TYPE} | awk -F': ' '/Manufacturer|Product Name|Serial Number/ {print $1": "$2}' | awk '{$1=$1};1' | sed 's/$/ |/' | paste -sd " " - | sed 's/ |$//')
- sed -i "/my \$dinfo = df('\/', 1);/i\\\t\t\$res->{systemInfo} = \"$(echo "$systemInfoCmd")\";\n" "$NODES_PM_FILE"
- msg "System information output added to \"$NODES_PM_FILE\"."
- fi
+# Collect system information
+collect_system_info() {
+ local output_file="$1"
+ local systemInfoCmd
- # Add new item to the items array in PVE.node.StatusView
- if [[ -z $(cat "$PVE_MANAGER_LIB_JS_FILE" | grep -e "itemId: 'thermal[[:alnum:]]*'") ]]; then
- local tempHelperCtorParams=$([[ "$TEMP_UNIT" = "F" ]] && echo '{srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: PVE.mod.TempHelper.FAHRENHEIT}' || echo '{srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: PVE.mod.TempHelper.CELSIUS}')
- # Expand space in StatusView
- sed -i "/Ext.define('PVE\.node\.StatusView'/,/\},/ {
- s/\(bodyPadding:\) '[^']*'/\1 '20 15 20 15'/
- s/height: [0-9]\+/minHeight: 360,\n\tflex: 1,\n\tcollapsible: true,\n\ttitleCollapse: true/
- s/\(tableAttrs:.*$\)/trAttrs: \{ valign: 'top' \},\n\t\1/
- }" "$PVE_MANAGER_LIB_JS_FILE"
- msg "Expanded space in \"$PVE_MANAGER_LIB_JS_FILE\"."
+ systemInfoCmd=$(dmidecode -t "${SYSTEM_INFO_TYPE}" \
+ | awk -F': ' '/Manufacturer|Product Name|Serial Number/ {print $1": "$2}' \
+ | awk '{$1=$1};1' \
+ | sed 's/$/ |/' \
+ | paste -sd " " - \
+ | sed 's/ |$//')
+ #region system info heredoc
+ sed -i "/my \$dinfo = df('\/', 1);/i\\\t\t\$res->{systemInfo} = \"$(echo "$systemInfoCmd")\";\n" "$NODES_PM_FILE"
+ #endregion system info heredoc
+ info "System information retriever added to \"$output_file\"."
+}
+#endregion node info insertion
- sed -i "/^Ext.define('PVE.node.StatusView'/i\
-Ext.define('PVE.mod.TempHelper', {\n\
- //singleton: true,\n\
-\n\
- requires: ['Ext.util.Format'],\n\
-\n\
- statics: {\n\
- CELSIUS: 0,\n\
- FAHRENHEIT: 1\n\
- },\n\
-\n\
- srcUnit: null,\n\
- dstUnit: null,\n\
-\n\
- isValidUnit: function (unit) {\n\
- return (\n\
- Ext.isNumber(unit) && (unit === this.self.CELSIUS || unit === this.self.FAHRENHEIT)\n\
- );\n\
- },\n\
-\n\
- constructor: function (config) {\n\
- this.srcUnit = config && this.isValidUnit(config.srcUnit) ? config.srcUnit : this.self.CELSIUS;\n\
- this.dstUnit = config && this.isValidUnit(config.dstUnit) ? config.dstUnit : this.self.CELSIUS;\n\
- },\n\
-\n\
- toFahrenheit: function (tempCelsius) {\n\
- return Ext.isNumber(tempCelsius)\n\
- ? tempCelsius * 9 / 5 + 32\n\
- : NaN;\n\
- },\n\
-\n\
- toCelsius: function (tempFahrenheit) {\n\
- return Ext.isNumber(tempFahrenheit)\n\
- ? (tempFahrenheit - 32) * 5 / 9\n\
- : NaN;\n\
- },\n\
-\n\
- getTemp: function (value) {\n\
- if (this.srcUnit !== this.dstUnit) {\n\
- switch (this.srcUnit) {\n\
- case this.self.CELSIUS:\n\
- switch (this.dstUnit) {\n\
- case this.self.FAHRENHEIT:\n\
- return this.toFahrenheit(value);\n\
-\n\
- default:\n\
- Ext.raise({\n\
- msg:\n\
- 'Unsupported destination temperature unit: ' + this.dstUnit,\n\
- });\n\
- }\n\
- case this.self.FAHRENHEIT:\n\
- switch (this.dstUnit) {\n\
- case this.self.CELSIUS:\n\
- return this.toCelsius(value);\n\
-\n\
- default:\n\
- Ext.raise({\n\
- msg:\n\
- 'Unsupported destination temperature unit: ' + this.dstUnit,\n\
- });\n\
- }\n\
- default:\n\
- Ext.raise({\n\
- msg: 'Unsupported source temperature unit: ' + this.srcUnit,\n\
- });\n\
- }\n\
- } else {\n\
- return value;\n\
- }\n\
- },\n\
-\n\
- getUnit: function(plainText) {\n\
- switch (this.dstUnit) {\n\
- case this.self.CELSIUS:\n\
- return plainText !== true ? '\°C' : '\\\'C';\n\
-\n\
- case this.self.FAHRENHEIT:\n\\n\
- return plainText !== true ? '\°F' : '\\\'F';\n\
-\n\
- default:\n\
- Ext.raise({\n\
- msg: 'Unsupported destination temperature unit: ' + this.srcUnit,\n\
- });\n\
- }\n\
- },\n\
-});\n" "$PVE_MANAGER_LIB_JS_FILE"
+#region widget generation functions
+# Helper function to insert widget after thermal items
+insert_widget_after_thermal() {
+ local widget_file="$1"
+ sed -i "/^Ext.define('PVE.node.StatusView',/ {
+ :a
+ /items:/!{N;ba;}
+ :b
+ /'cpus.*},/!{N;bb;}
+ r $widget_file
+ }" "$PVE_MANAGER_LIB_JS_FILE"
+}
- if [ $ENABLE_SYSTEM_INFO = true ]; then
- sed -i "/^Ext.define('PVE.node.StatusView',/ {
- :a;
- /items:/!{N;ba;}
- :b;
- /cpus.*},/!{N;bb;}
- a\
- \\
- {\n\
- itemId: 'sysinfo',\n\
- colspan: 2,\n\
- printBar: false,\n\
- title: gettext('System Information'),\n\
- textField: 'systemInfo',\n\
- renderer: function(value){\n\
- return value;\n\
- }\n\
- },
- }" "$PVE_MANAGER_LIB_JS_FILE"
- fi
-
- sed -i "/^Ext.define('PVE.node.StatusView',/ {
- :a;
- /items:/!{N;ba;}
- :b;
- /cpus.*},/!{N;bb;}
- a\
- \\
- {\n\
- itemId: 'thermalCpu',\n\
- colspan: 2,\n\
- printBar: false,\n\
- title: gettext('CPU Thermal State'),\n\
- iconCls: 'fa fa-fw fa-thermometer-half',\n\
- textField: 'sensorsOutput',\n\
- renderer: function(value){\n\
- // sensors configuration\n\
- const cpuTempHelper = Ext.create('PVE.mod.TempHelper', $tempHelperCtorParams);\n\
- // display configuration\n\
- const itemsPerRow = $CPU_ITEMS_PER_ROW;\n\
- // ---\n\
- let objValue;\n\
- try {\n\
- objValue = JSON.parse(value) || {};\n\
- } catch(e) {\n\
- objValue = {};\n\
- }\n\
- const cpuKeysI = Object.keys(objValue).filter(item => String(item).startsWith('coretemp-isa-')).sort();\n\
- const cpuKeysA = Object.keys(objValue).filter(item => String(item).startsWith('k10temp-pci-')).sort();\n\
- const bINTEL = cpuKeysI.length > 0 ? true : false;\n\
- const INTELPackagePrefix = '$CPU_TEMP_TARGET' == 'Core' ? 'Core ' : 'Package id';\n\
- const INTELPackageCaption = '$CPU_TEMP_TARGET' == 'Core' ? 'Core' : 'Package';\n\
- let AMDPackagePrefix = 'Tccd';\n\
- let AMDPackageCaption = 'Chiplet';\n\
- if (cpuKeysA.length > 0) {\n\
- let bTccd = false;\n\
- let bTctl = false;\n\
- let bTdie = false;\n\
- cpuKeysA.forEach((cpuKey, cpuIndex) => {\n\
- let items = objValue[cpuKey];\n\
- bTccd = Object.keys(items).findIndex(item => { return String(item).startsWith('Tccd'); }) >= 0;\n\
- bTctl = Object.keys(items).findIndex(item => { return String(item).startsWith('Tctl'); }) >= 0;\n\
- bTdie = Object.keys(items).findIndex(item => { return String(item).startsWith('Tdie'); }) >= 0;\n\
- });\n\
- if (bTccd && bTctl && '$CPU_TEMP_TARGET' == 'Core') {\n\
- AMDPackagePrefix = 'Tccd';\n\
- AMDPackageCaption = 'Chiplet';\n\
- } else if (bTdie) {\n\
- AMDPackagePrefix = 'Tdie';\n\
- AMDPackageCaption = 'Temp';\n\
- } else if (bTctl) {\n\
- AMDPackagePrefix = 'Tctl';\n\
- AMDPackageCaption = 'Temp';\n\
- } else {\n\
- AMDPackagePrefix = 'temp';\n\
- AMDPackageCaption = 'Temp';\n\
- }\n\
- }\n\
- const cpuKeys = bINTEL ? cpuKeysI : cpuKeysA;\n\
- const cpuItemPrefix = bINTEL ? INTELPackagePrefix : AMDPackagePrefix;\n\
- const cpuTempCaption = bINTEL ? INTELPackageCaption : AMDPackageCaption;\n\
- const formatTemp = bINTEL ? '0' : '0.0';\n\
- const cpuCount = cpuKeys.length;\n\
- let temps = [];\n\
- cpuKeys.forEach((cpuKey, cpuIndex) => {\n\
- let cpuTemps = [];\n\
- const items = objValue[cpuKey];\n\
- const itemKeys = Object.keys(items).filter(item => { return String(item).includes(cpuItemPrefix); });\n\
- itemKeys.forEach((coreKey) => {\n\
- try {\n\
- let tempVal = NaN, tempMax = NaN, tempCrit = NaN;\n\
- Object.keys(items[coreKey]).forEach((secondLevelKey) => {\n\
- if (secondLevelKey.endsWith('_input')) {\n\
- tempVal = cpuTempHelper.getTemp(parseFloat(items[coreKey][secondLevelKey]));\n\
- } else if (secondLevelKey.endsWith('_max')) {\n\
- tempMax = cpuTempHelper.getTemp(parseFloat(items[coreKey][secondLevelKey]));\n\
- } else if (secondLevelKey.endsWith('_crit')) {\n\
- tempCrit = cpuTempHelper.getTemp(parseFloat(items[coreKey][secondLevelKey]));\n\
- }\n\
- });\n\
- if (!isNaN(tempVal)) {\n\
- let tempStyle = '';\n\
- if (!isNaN(tempMax) && tempVal >= tempMax) {\n\
- tempStyle = 'color: #FFC300; font-weight: bold;';\n\
- }\n\
- if (!isNaN(tempCrit) && tempVal >= tempCrit) {\n\
- tempStyle = 'color: red; font-weight: bold;';\n\
- }\n\
- let tempStr = '';\n\
- let tempIndex = coreKey.match(\/(?:P\\\s+Core|E\\\s+Core|Core)\\\s*(\\\d+)\/);\n\
- if (tempIndex !== null && tempIndex.length > 1) {\n\
- tempIndex = tempIndex[1];\n\
- let coreType = coreKey.startsWith('P Core') ? 'P Core' :\n\
- coreKey.startsWith('E Core') ? 'E Core' :\n\
- cpuTempCaption;\n\
- tempStr = \`\${coreType} \${tempIndex}: \${Ext.util.Format.number(tempVal, formatTemp)}\${cpuTempHelper.getUnit()}\`;\n\
- } else {\n\
- // fallback for CPUs which do not have a core index\n\
- let coreType = coreKey.startsWith('P Core') ? 'P Core' :\n\
- coreKey.startsWith('E Core') ? 'E Core' :\n\
- cpuTempCaption;\n\
- tempStr = \`\${coreType}: \${Ext.util.Format.number(tempVal, formatTemp)}\${cpuTempHelper.getUnit()}\`;\n\
- }\n\
- cpuTemps.push(tempStr);\n\
- }\n\
- } catch (e) { /*_*/ }\n\
- });\n\
- if(cpuTemps.length > 0) {\n\
- temps.push(cpuTemps);\n\
- }\n\
- });\n\
- let result = '';\n\
- temps.forEach((cpuTemps, cpuIndex) => {\n\
- const strCoreTemps = cpuTemps.map((strTemp, index, arr) => { return strTemp + (index + 1 < arr.length ? (itemsPerRow > 0 && (index + 1) % itemsPerRow === 0 ? '
' : ' | ') : ''); })\n\
- if(strCoreTemps.length > 0) {\n\
- result += (cpuCount > 1 ? \`CPU \${cpuIndex+1}: \` : '') + strCoreTemps.join('') + (cpuIndex < cpuCount ? '
' : '');\n\
- }\n\
- });\n\
- return '
' + (result.length > 0 ? result : 'N/A') + '
';\n\
- }\n\
- },
- }" "$PVE_MANAGER_LIB_JS_FILE"
-
- #
- # NOTE: The following items will be added in reverse order
- #
-
- if [ $ENABLE_UPS = true ]; then
- local TEMP_JS_FILE="/tmp/ups_widget.js"
- generate_ups_widget $TEMP_JS_FILE
-
- sed -i "/^Ext.define('PVE.node.StatusView',/ {
- :a
- /items:/!{N;ba;}
- :b
- /'thermal.*},/!{N;bb;}
- r /tmp/ups_widget.js
- }" "$PVE_MANAGER_LIB_JS_FILE"
-
- rm $TEMP_JS_FILE
- fi
-
- if [ $ENABLE_HDD_TEMP = true ]; then
- sed -i "/^Ext.define('PVE.node.StatusView',/ {
- :a;
- /items:/!{N;ba;}
- :b;
- /'thermal.*},/!{N;bb;}
- a\
- \\
- {\n\
- itemId: 'thermalHdd',\n\
- colspan: 2,\n\
- printBar: false,\n\
- title: gettext('HDD/SSD Thermal State'),\n\
- iconCls: 'fa fa-fw fa-thermometer-half',\n\
- textField: 'sensorsOutput',\n\
- renderer: function(value) {\n\
- // sensors configuration\n\
- const addressPrefix = \"drivetemp-scsi-\";\n\
- const sensorName = \"temp1\";\n\
- const tempHelper = Ext.create('PVE.mod.TempHelper', $tempHelperCtorParams);\n\
- // display configuration\n\
- const itemsPerRow = ${HDD_ITEMS_PER_ROW};\n\
- // ---\n\
- let objValue;\n\
- try {\n\
- objValue = JSON.parse(value) || {};\n\
- } catch(e) {\n\
- objValue = {};\n\
- }\n\
- const drvKeys = Object.keys(objValue).filter(item => String(item).startsWith(addressPrefix)).sort();\n\
- let temps = [];\n\
- drvKeys.forEach((drvKey, index) => {\n\
- try {\n\
- let tempVal = NaN, tempMax = NaN, tempCrit = NaN;\n\
- Object.keys(objValue[drvKey][sensorName]).forEach((secondLevelKey) => {\n\
- if (secondLevelKey.endsWith('_input')) {\n\
- tempVal = tempHelper.getTemp(parseFloat(objValue[drvKey][sensorName][secondLevelKey]));\n\
- } else if (secondLevelKey.endsWith('_max')) {\n\
- tempMax = tempHelper.getTemp(parseFloat(objValue[drvKey][sensorName][secondLevelKey]));\n\
- } else if (secondLevelKey.endsWith('_crit')) {\n\
- tempCrit = tempHelper.getTemp(parseFloat(objValue[drvKey][sensorName][secondLevelKey]));\n\
- }\n\
- });\n\
- if (!isNaN(tempVal)) {\n\
- let tempStyle = '';\n\
- if (!isNaN(tempMax) && tempVal >= tempMax) {\n\
- tempStyle = 'color: #FFC300; font-weight: bold;';\n\
- }\n\
- if (!isNaN(tempCrit) && tempVal >= tempCrit) {\n\
- tempStyle = 'color: red; font-weight: bold;';\n\
- }\n\
- const tempStr = \`Drive \${index + 1}: \${Ext.util.Format.number(tempVal, '0.0')}\${tempHelper.getUnit()}\`;\n\
- temps.push(tempStr);\n\
- }\n\
- } catch(e) { /*_*/ }\n\
- });\n\
- const result = temps.map((strTemp, index, arr) => { return strTemp + (index + 1 < arr.length ? ((index + 1) % itemsPerRow === 0 ? '
' : ' | ') : ''); });\n\
- return '' + (result.length > 0 ? result.join('') : 'N/A') + '
';\n\
- }\n\
- },
- }" "$PVE_MANAGER_LIB_JS_FILE"
- fi
-
- if [ $ENABLE_NVME_TEMP = true ]; then
- sed -i "/^Ext.define('PVE.node.StatusView',/ {
- :a;
- /items:/!{N;ba;}
- :b;
- /'thermal.*},/!{N;bb;}
- a\
- \\
- {\n\
- itemId: 'thermalNvme',\n\
- colspan: 2,\n\
- printBar: false,\n\
- title: gettext('NVMe Thermal State'),\n\
- iconCls: 'fa fa-fw fa-thermometer-half',\n\
- textField: 'sensorsOutput',\n\
- renderer: function(value) {\n\
- // sensors configuration\n\
- const addressPrefix = \"nvme-pci-\";\n\
- const sensorName = \"Composite\";\n\
- const tempHelper = Ext.create('PVE.mod.TempHelper', $tempHelperCtorParams);\n\
- // display configuration\n\
- const itemsPerRow = ${NVME_ITEMS_PER_ROW};\n\
- // ---\n\
- let objValue;\n\
- try {\n\
- objValue = JSON.parse(value) || {};\n\
- } catch(e) {\n\
- objValue = {};\n\
- }\n\
- const nvmeKeys = Object.keys(objValue).filter(item => String(item).startsWith(addressPrefix)).sort();\n\
- let temps = [];\n\
- nvmeKeys.forEach((nvmeKey, index) => {\n\
- try {\n\
- let tempVal = NaN, tempMax = NaN, tempCrit = NaN;\n\
- Object.keys(objValue[nvmeKey][sensorName]).forEach((secondLevelKey) => {\n\
- if (secondLevelKey.endsWith('_input')) {\n\
- tempVal = tempHelper.getTemp(parseFloat(objValue[nvmeKey][sensorName][secondLevelKey]));\n\
- } else if (secondLevelKey.endsWith('_max')) {\n\
- tempMax = tempHelper.getTemp(parseFloat(objValue[nvmeKey][sensorName][secondLevelKey]));\n\
- } else if (secondLevelKey.endsWith('_crit')) {\n\
- tempCrit = tempHelper.getTemp(parseFloat(objValue[nvmeKey][sensorName][secondLevelKey]));\n\
- }\n\
- });\n\
- if (!isNaN(tempVal)) {\n\
- let tempStyle = '';\n\
- if (!isNaN(tempMax) && tempVal >= tempMax) {\n\
- tempStyle = 'color: #FFC300; font-weight: bold;';\n\
- }\n\
- if (!isNaN(tempCrit) && tempVal >= tempCrit) {\n\
- tempStyle = 'color: red; font-weight: bold;';\n\
- }\n\
- const tempStr = \`Drive \${index + 1}: \${Ext.util.Format.number(tempVal, '0.0')}\${tempHelper.getUnit()}\`;\n\
- temps.push(tempStr);\n\
- }\n\
- } catch(e) { /*_*/ }\n\
- });\n\
- const result = temps.map((strTemp, index, arr) => { return strTemp + (index + 1 < arr.length ? ((index + 1) % itemsPerRow === 0 ? '
' : ' | ') : ''); });\n\
- return '' + (result.length > 0 ? result.join('') : 'N/A') + '
';\n\
- }\n\
- },
- }" "$PVE_MANAGER_LIB_JS_FILE"
- fi
-
- if [ $ENABLE_NVME_TEMP = true -o $ENABLE_HDD_TEMP = true ]; then
- sed -i "/^Ext.define('PVE.node.StatusView',/ {
- :a;
- /items:/!{N;ba;}
- :b;
- /'thermal.*},/!{N;bb;}
- a\
- \\
- {\n\
- xtype: 'box',\n\
- colspan: 2,\n\
- html: gettext('Drive(s)'),\n\
- },
- }" "$PVE_MANAGER_LIB_JS_FILE"
- fi
-
- if [ $ENABLE_FAN_SPEED = true ]; then
- # Add fan speeds display
- sed -i "/^Ext.define('PVE.node.StatusView',/ {
- :a;
- /items:/!{N;ba;}
- :b;
- /'thermal.*},/!{N;bb;}
- a\
- \\
- {\n\
- xtype: 'box',\n\
- colspan: 2,\n\
- html: gettext('Cooling'),\n\
- },\n\
- {\n\
- itemId: 'speedFan',\n\
- colspan: 2,\n\
- printBar: false,\n\
- title: gettext('Fan Speed(s)'),\n\
- iconCls: 'fa fa-fw fa-snowflake-o',\n\
- textField: 'sensorsOutput',\n\
- renderer: function(value) {\n\
- // ---\n\
- let objValue;\n\
- try {\n\
- objValue = JSON.parse(value) || {};\n\
- } catch(e) {\n\
- objValue = {};\n\
- }\n\
-\n\
- // Recursive function to find fan keys and values\n\
- function findFanKeys(obj, fanKeys, parentKey = null) {\n\
- Object.keys(obj).forEach(key => {\n\
- const value = obj[key];\n\
- if (typeof value === 'object' && value !== null) {\n\
- // If the value is an object, recursively call the function\n\
- findFanKeys(value, fanKeys, key);\n\
- } else if (/^fan[0-9]+(_input)?$/.test(key)) {\n\
- if ($DISPLAY_ZERO_SPEED_FANS != true && value === 0) {\n\
- // Skip this fan if DISPLAY_ZERO_SPEED_FANS is false and value is 0\n\
- return;\n\
- }\n\
- // If the key matches the pattern, add the parent key and value to the fanKeys array\n\
- fanKeys.push({ key: parentKey, value: value });\n\
- }\n\
- });\n\
- }\n\
-\n\
- let speeds = [];\n\
- // Loop through the parent keys\n\
- Object.keys(objValue).forEach(parentKey => {\n\
- const parentObj = objValue[parentKey];\n\
- // Array to store fan keys and values\n\
- const fanKeys = [];\n\
- // Call the recursive function to find fan keys and values\n\
- findFanKeys(parentObj, fanKeys);\n\
- // Sort the fan keys\n\
- fanKeys.sort();\n\
- // Process each fan key and value\n\
- fanKeys.forEach(({ key: fanKey, value: fanSpeed }) => {\n\
- try {\n\
- const fan = fanKey.charAt(0).toUpperCase() + fanKey.slice(1); // Capitalize the first letter of fanKey\n\
- speeds.push(\`\${fan}: \${fanSpeed} RPM\`);\n\
- } catch(e) {\n\
- console.error(\`Error retrieving fan speed for \${fanKey} in \${parentKey}:\`, e); // Debug: Log specific error\n\
- }\n\
- });\n\
- });\n\
- return '' + (speeds.length > 0 ? speeds.join(' | ') : 'N/A') + '
';\n\
- }\n\
- },
- }" "$PVE_MANAGER_LIB_JS_FILE"
- fi
-
- if [ $ENABLE_RAM_TEMP = true ]; then
- # Add Ram temperature display
- sed -i "/^Ext.define('PVE.node.StatusView',/ {
- :a;
- /items:/!{N;ba;}
- :b;
- /'thermal.*},/!{N;bb;}
- a\
- \\
- {\n\
- xtype: 'box',\n\
- colspan: 2,\n\
- html: gettext('RAM'),\n\
- },\n\
- {\n\
- itemId: 'thermalRam',\n\
- colspan: 2,\n\
- printBar: false,\n\
- title: gettext('Thermal State'),\n\
- iconCls: 'fa fa-fw fa-thermometer-half',\n\
- textField: 'sensorsOutput',\n\
- renderer: function(value) {\n\
- const cpuTempHelper = Ext.create('PVE.mod.TempHelper', {srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: PVE.mod.TempHelper.CELSIUS});\n\
- // Make SODIMM unique keys\n\
- value = value.split('\\\n'); // Split by newlines\n\
- for (let i = 0; i < value.length; i++) {\n\
- // Check if the current line contains 'SODIMM'\n\
- if (value[i].includes('SODIMM') && i + 1 < value.length) {\n\
- // Extract the number '3' following 'temp' from the next line (e.g., "temp3_input": 25.000)\n\
- let nextLine = value[i + 1];\n\
- let match = nextLine.match(/\"temp(\\\d+)_input\": (\\\d+\\\.\\\d+)/);\n\
-\n\
- if (match) {\n\
- let number = match[1]; // Extracted number\n\
- // Replace the current line with SODIMM by the extracted number\n\
- value[i] = value[i].replace('SODIMM', \`SODIMM\${number}\`);\n\
- }\n\
- }\n\
- }\n\
- value = value.join('\\\n'); // Reverse line split\n\
-\n\
- let objValue;\n\
- try {\n\
- objValue = JSON.parse(value) || {};\n\
- } catch(e) {\n\
- objValue = {};\n\
- }\n\
-\n\
- // Recursive function to find ram keys and values\n\
- function findRamKeys(obj, ramKeys, parentKey = null) {\n\
- Object.keys(obj).forEach(key => {\n\
- const value = obj[key];\n\
- if (typeof value === 'object' && value !== null) {\n\
- // If the value is an object, recursively call the function\n\
- findRamKeys(value, ramKeys, key);\n\
- } else if (/^temp\\\d+_input$/.test(key) && parentKey && parentKey.startsWith(\"SODIMM\")) {\n\
- if (value !== 0) {\n\
- ramKeys.push({ key: parentKey, value: value});\n\
- }\n\
- }\n\
- });\n\
- }\n\
-\n\
- let ramTemps = [];\n\
- // Loop through the parent keys\n\
- Object.keys(objValue).forEach(parentKey => {\n\
- const parentObj = objValue[parentKey];\n\
- // Array to store ram keys and values\n\
- const ramKeys = [];\n\
- // Call the recursive function to find ram keys and values\n\
- findRamKeys(parentObj, ramKeys);\n\
- // Sort the ramKeys keys\n\
- ramKeys.sort();\n\
- // Process each ram key and value\n\
- ramKeys.forEach(({ key: ramKey, value: ramTemp }) => {\n\
- try {\n\
- ram = ramKey.replace('SODIMM', 'SODIMM ');\n\
- ramTemps.push(\`\${ram}: \${ramTemp}\${cpuTempHelper.getUnit()}\`);\n\
- } catch(e) {\n\
- console.error(\`Error retrieving Ram Temp for \${ramTemps} in \${parentKey}:\`, e); // Debug: Log specific error\n\
- }\n\
- });\n\
- });\n\
- return '' + (ramTemps.length > 0 ? ramTemps.join(' | ') : 'N/A') + '
';\n\
- }\n\
- },
- }" "$PVE_MANAGER_LIB_JS_FILE"
- fi
-
- # Add an empty line to separate modified items as a visual group
- # NOTE: Check for the presence of items in the reverse order of display
- local lastItemId=""
- if [ $ENABLE_UPS = true ]; then
- lastItemId="upsc"
- elif [ $ENABLE_HDD_TEMP = true ]; then
- lastItemId="thermalHdd"
- elif [ $ENABLE_NVME_TEMP = true ]; then
- lastItemId="thermalNvme"
- elif [ $ENABLE_FAN_SPEED = true ]; then
- lastItemId="speedFan"
- else
- lastItemId="thermalCpu"
- fi
-
- if [ -n "$lastItemId" ]; then
- sed -i "/^Ext.define('PVE.node.StatusView',/ {
- :a;
- /^.*{.*'$lastItemId'.*},/!{N;ba;}
- a\
- \\
- {\n\
- xtype: 'box',\n\
- colspan: 2,\n\
- padding: '0 0 20 0',\n\
- },
- }" "$PVE_MANAGER_LIB_JS_FILE"
- fi
-
- # Move the node summary box into its own container
- sed -i "/^\s*nodeStatus: nodeStatus,/ {
- :a
- /items: \[/ !{N;ba;}
- a\
- \\
- {\n\
- xtype: 'container',\n\
- itemId: 'summarycontainer',\n\
- layout: 'column',\n\
- minWidth: 700,\n\
- defaults: {\n\
- minHeight: 350,\n\
- padding: 5,\n\
- columnWidth: 1,\n\
- },\n\
- items: [\n\
- nodeStatus,\n\
- ]\n\
- },
- }" "$PVE_MANAGER_LIB_JS_FILE"
-
- # Deactivate the original box instance
- sed -i "/^\s*nodeStatus: nodeStatus,/ {
- :a
- /itemId: 'itemcontainer',/ !{N;ba;}
- n;
- :b
- /nodeStatus,/ !{N;bb;}
- s/nodeStatus/\/\/nodeStatus/
- }" "$PVE_MANAGER_LIB_JS_FILE"
-
- msg "Sensor display items added to the summary panel in \"$PVE_MANAGER_LIB_JS_FILE\"."
-
- restart_proxy
-
- msg "Installation completed."
-
- info "Clear the browser cache to ensure all changes are visualized."
- else
- warn "Sensor display items already added to the summary panel in \"$PVE_MANAGER_LIB_JS_FILE\"."
+# Helper function to generate widget and insert it
+generate_and_insert_widget() {
+ local enable_flag="$1"
+ local generator_func="$2"
+ local widget_name="$3"
+
+ if [ "$enable_flag" = true ]; then
+ local temp_js_file="/tmp/${widget_name}_widget.js"
+ "$generator_func" "$temp_js_file"
+ insert_widget_after_thermal "$temp_js_file"
+ rm "$temp_js_file"
fi
}
+# Function to generate drive header
+generate_drive_header() {
+ if [ "$ENABLE_NVME_TEMP" = true ] || [ "$ENABLE_HDD_TEMP" = true ]; then
+ local temp_js_file="/tmp/drive_header.js"
+ #region drive header heredoc
+ cat > "$temp_js_file" <<'EOF'
+{
+ xtype: 'box',
+ colspan: 2,
+ html: gettext('Drive(s)'),
+},
+EOF
+#endregion drive header heredoc
+ if [[ $? -ne 0 ]]; then
+ echo "Error: Failed to generate drive header code" >&2
+ exit 1
+ fi
+
+ insert_widget_after_thermal "$temp_js_file"
+ rm "$temp_js_file"
+ fi
+}
+
+# Function to expand space and modify StatusView properties
+expand_statusview_space() {
+ msgb "\n=== Expanding StatusView space ==="
+
+ # Apply multiple modifications to the StatusView definition
+ sed -i "/Ext.define('PVE\.node\.StatusView'/,/\},/ {
+ s/\(bodyPadding:\) '[^']*'/\1 '20 15 20 15'/
+ s/height: [0-9]\+/minHeight: 360,\n\tflex: 1,\n\tcollapsible: true,\n\ttitleCollapse: true/
+ s/\(tableAttrs:.*$\)/trAttrs: \{ valign: 'top' \},\n\t\1/
+ }" "$PVE_MANAGER_LIB_JS_FILE"
+
+ if [[ $? -ne 0 ]]; then
+ echo "Error: Failed to expand StatusView space" >&2
+ exit 1
+ fi
+
+ info "Expanded space in \"$PVE_MANAGER_LIB_JS_FILE\"."
+}
+
+# Function to move node summary into its own container
+setup_node_summary_container() {
+ # Move the node summary box into its own container
+ local temp_js_file="/tmp/summary_container.js"
+ #region summary container heredoc
+ cat > "$temp_js_file" <<'EOF'
+{
+ xtype: 'container',
+ itemId: 'summarycontainer',
+ layout: 'column',
+ minWidth: 700,
+ defaults: {
+ minHeight: 350,
+ padding: 5,
+ columnWidth: 1,
+ },
+ items: [
+ nodeStatus,
+ ]
+},
+EOF
+#endregion summary container heredoc
+ if [[ $? -ne 0 ]]; then
+ echo "Error: Failed to generate summary container code" >&2
+ exit 1
+ fi
+
+ # Insert the new container after finding the nodeStatus and items pattern
+ sed -i "/^\s*nodeStatus: nodeStatus,/ {
+ :a
+ /items: \[/ !{N;ba;}
+ r $temp_js_file
+ }" "$PVE_MANAGER_LIB_JS_FILE"
+
+ rm "$temp_js_file"
+
+ # Deactivate the original box instance
+ sed -i "/^\s*nodeStatus: nodeStatus,/ {
+ :a
+ /itemId: 'itemcontainer',/ !{N;ba;}
+ n;
+ :b
+ /nodeStatus,/ !{N;bb;}
+ s/nodeStatus/\/\/nodeStatus/
+ }" "$PVE_MANAGER_LIB_JS_FILE"
+
+ if [[ $? -ne 0 ]]; then
+ echo "Error: Failed to deactivate original nodeStatus instance" >&2
+ exit 1
+ fi
+}
+
+# Function to add visual spacing separator after the last widget
+add_visual_separator() {
+ # Check for the presence of items in the reverse order of display
+ local lastItemId=""
+
+ if [ "$ENABLE_UPS" = true ]; then
+ lastItemId="upsc"
+ elif [ "$ENABLE_HDD_TEMP" = true ]; then
+ lastItemId="thermalHdd"
+ elif [ "$ENABLE_NVME_TEMP" = true ]; then
+ lastItemId="thermalNvme"
+ elif [ "$ENABLE_FAN_SPEED" = true ]; then
+ lastItemId="speedFan"
+ else
+ lastItemId="thermalCpu"
+ fi
+
+ if [ -n "$lastItemId" ]; then
+ local temp_js_file="/tmp/visual_separator.js"
+
+ #region visual spacing heredoc
+ cat > "$temp_js_file" <<'EOF'
+{
+ xtype: 'box',
+ colspan: 2,
+ padding: '0 0 20 0',
+},
+EOF
+#endregion visual spacing heredoc
+ if [[ $? -ne 0 ]]; then
+ echo "Error: Failed to generate visual separator code" >&2
+ exit 1
+ fi
+
+ # Insert after the specific lastItemId (different pattern than thermal)
+ sed -i "/^Ext.define('PVE.node.StatusView',/ {
+ :a;
+ /^.*{.*'$lastItemId'.*},/!{N;ba;}
+ r $temp_js_file
+ }" "$PVE_MANAGER_LIB_JS_FILE"
+
+ rm "$temp_js_file"
+ fi
+}
+
+# Function to generate system info widget
+generate_system_info() {
+ #region system info heredoc
+ cat > "$1" <<'EOF'
+ {
+ itemId: 'sysinfo',
+ colspan: 2,
+ printBar: false,
+ title: gettext('System Information'),
+ textField: 'systemInfo',
+ renderer: function(value){
+ return value;
+ }
+ },
+EOF
+ #endregion system info heredoc
+ if [[ $? -ne 0 ]]; then
+ echo "Error: Failed to generate system info code" >&2
+ exit 1
+ fi
+}
+
+# Function to generate and insert temperature conversion helper class
+generate_and_insert_temp_helper() {
+ local temp_js_file="/tmp/temp_helper.js"
+
+ msgb "\n=== Inserting temperature helper ==="
+
+ #region temp helper heredoc
+ cat > "$temp_js_file" <<'EOF'
+Ext.define('PVE.mod.TempHelper', {
+ //singleton: true,
+
+ requires: ['Ext.util.Format'],
+
+ statics: {
+ CELSIUS: 0,
+ FAHRENHEIT: 1
+ },
+
+ srcUnit: null,
+ dstUnit: null,
+
+ isValidUnit: function (unit) {
+ return (
+ Ext.isNumber(unit) && (unit === this.self.CELSIUS || unit === this.self.FAHRENHEIT)
+ );
+ },
+
+ constructor: function (config) {
+ this.srcUnit = config && this.isValidUnit(config.srcUnit) ? config.srcUnit : this.self.CELSIUS;
+ this.dstUnit = config && this.isValidUnit(config.dstUnit) ? config.dstUnit : this.self.CELSIUS;
+ },
+
+ toFahrenheit: function (tempCelsius) {
+ return Ext.isNumber(tempCelsius)
+ ? tempCelsius * 9 / 5 + 32
+ : NaN;
+ },
+
+ toCelsius: function (tempFahrenheit) {
+ return Ext.isNumber(tempFahrenheit)
+ ? (tempFahrenheit - 32) * 5 / 9
+ : NaN;
+ },
+
+ getTemp: function (value) {
+ if (this.srcUnit !== this.dstUnit) {
+ switch (this.srcUnit) {
+ case this.self.CELSIUS:
+ switch (this.dstUnit) {
+ case this.self.FAHRENHEIT:
+ return this.toFahrenheit(value);
+
+ default:
+ Ext.raise({
+ msg:
+ 'Unsupported destination temperature unit: ' + this.dstUnit,
+ });
+ }
+ case this.self.FAHRENHEIT:
+ switch (this.dstUnit) {
+ case this.self.CELSIUS:
+ return this.toCelsius(value);
+
+ default:
+ Ext.raise({
+ msg:
+ 'Unsupported destination temperature unit: ' + this.dstUnit,
+ });
+ }
+ default:
+ Ext.raise({
+ msg: 'Unsupported source temperature unit: ' + this.srcUnit,
+ });
+ }
+ } else {
+ return value;
+ }
+ },
+
+ getUnit: function(plainText) {
+ switch (this.dstUnit) {
+ case this.self.CELSIUS:
+ return plainText !== true ? '°C' : '\'C';
+
+ case this.self.FAHRENHEIT:
+ return plainText !== true ? '°F' : '\'F';
+
+ default:
+ Ext.raise({
+ msg: 'Unsupported destination temperature unit: ' + this.srcUnit,
+ });
+ }
+ },
+});
+EOF
+ #endregion temp helper heredoc
+ if [[ $? -ne 0 ]]; then
+ echo "Error: Failed to generate temp helper code" >&2
+ exit 1
+ fi
+
+ sed -i "/^Ext.define('PVE.node.StatusView'/e cat /tmp/temp_helper.js" "$PVE_MANAGER_LIB_JS_FILE"
+ rm "$temp_js_file"
+
+ info "Temperature helper inserted successfully."
+}
+
+# Function to generate CPU widget
+generate_cpu_widget() {
+ #region cpu widget heredoc
+ # use subshell to allow variable expansion
+ (
+ export CPU_ITEMS_PER_ROW
+ export CPU_TEMP_TARGET
+ export HELPERCTORPARAMS
+
+ cat <<'EOF' | envsubst '$CPU_ITEMS_PER_ROW $CPU_TEMP_TARGET $HELPERCTORPARAMS' > "$1"
+ {
+ itemId: 'thermalCpu',
+ colspan: 2,
+ printBar: false,
+ title: gettext('CPU Thermal State'),
+ iconCls: 'fa fa-fw fa-thermometer-half',
+ textField: 'sensorsOutput',
+ renderer: function(value){
+ // sensors configuration
+ const cpuTempHelper = Ext.create('PVE.mod.TempHelper', $HELPERCTORPARAMS);
+ // display configuration
+ const itemsPerRow = $CPU_ITEMS_PER_ROW;
+ // ---
+ let objValue;
+ try {
+ objValue = JSON.parse(value) || {};
+ } catch(e) {
+ objValue = {};
+ }
+ const cpuKeysI = Object.keys(objValue).filter(item => String(item).startsWith('coretemp-isa-')).sort();
+ const cpuKeysA = Object.keys(objValue).filter(item => String(item).startsWith('k10temp-pci-')).sort();
+ const bINTEL = cpuKeysI.length > 0 ? true : false;
+ const INTELPackagePrefix = '$CPU_TEMP_TARGET' == 'Core' ? 'Core ' : 'Package id';
+ const INTELPackageCaption = '$CPU_TEMP_TARGET' == 'Core' ? 'Core' : 'Package';
+ let AMDPackagePrefix = 'Tccd';
+ let AMDPackageCaption = 'Chiplet';
+ if (cpuKeysA.length > 0) {
+ let bTccd = false;
+ let bTctl = false;
+ let bTdie = false;
+ cpuKeysA.forEach((cpuKey, cpuIndex) => {
+ let items = objValue[cpuKey];
+ bTccd = Object.keys(items).findIndex(item => { return String(item).startsWith('Tccd'); }) >= 0;
+ bTctl = Object.keys(items).findIndex(item => { return String(item).startsWith('Tctl'); }) >= 0;
+ bTdie = Object.keys(items).findIndex(item => { return String(item).startsWith('Tdie'); }) >= 0;
+ });
+ if (bTccd && bTctl && '$CPU_TEMP_TARGET' == 'Core') {
+ AMDPackagePrefix = 'Tccd';
+ AMDPackageCaption = 'Chiplet';
+ } else if (bTdie) {
+ AMDPackagePrefix = 'Tdie';
+ AMDPackageCaption = 'Temp';
+ } else if (bTctl) {
+ AMDPackagePrefix = 'Tctl';
+ AMDPackageCaption = 'Temp';
+ } else {
+ AMDPackagePrefix = 'temp';
+ AMDPackageCaption = 'Temp';
+ }
+ }
+ const cpuKeys = bINTEL ? cpuKeysI : cpuKeysA;
+ const cpuItemPrefix = bINTEL ? INTELPackagePrefix : AMDPackagePrefix;
+ const cpuTempCaption = bINTEL ? INTELPackageCaption : AMDPackageCaption;
+ const formatTemp = bINTEL ? '0' : '0.0';
+ const cpuCount = cpuKeys.length;
+ let temps = [];
+ cpuKeys.forEach((cpuKey, cpuIndex) => {
+ let cpuTemps = [];
+ const items = objValue[cpuKey];
+ const itemKeys = Object.keys(items).filter(item => { return String(item).includes(cpuItemPrefix); });
+ itemKeys.forEach((coreKey) => {
+ try {
+ let tempVal = NaN, tempMax = NaN, tempCrit = NaN;
+ Object.keys(items[coreKey]).forEach((secondLevelKey) => {
+ if (secondLevelKey.endsWith('_input')) {
+ tempVal = cpuTempHelper.getTemp(parseFloat(items[coreKey][secondLevelKey]));
+ } else if (secondLevelKey.endsWith('_max')) {
+ tempMax = cpuTempHelper.getTemp(parseFloat(items[coreKey][secondLevelKey]));
+ } else if (secondLevelKey.endsWith('_crit')) {
+ tempCrit = cpuTempHelper.getTemp(parseFloat(items[coreKey][secondLevelKey]));
+ }
+ });
+ if (!isNaN(tempVal)) {
+ let tempStyle = '';
+ if (!isNaN(tempMax) && tempVal >= tempMax) {
+ tempStyle = 'color: #FFC300; font-weight: bold;';
+ }
+ if (!isNaN(tempCrit) && tempVal >= tempCrit) {
+ tempStyle = 'color: red; font-weight: bold;';
+ }
+ let tempStr = '';
+ let tempIndex = coreKey.match(/(?:P\s+Core|E\s+Core|Core)\s*(\d+)/);
+ if (tempIndex !== null && tempIndex.length > 1) {
+ tempIndex = tempIndex[1];
+ let coreType = coreKey.startsWith('P Core') ? 'P Core' :
+ coreKey.startsWith('E Core') ? 'E Core' :
+ cpuTempCaption;
+ tempStr = `${coreType} ${tempIndex}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()}`;
+ } else {
+ // fallback for CPUs which do not have a core index
+ let coreType = coreKey.startsWith('P Core') ? 'P Core' :
+ coreKey.startsWith('E Core') ? 'E Core' :
+ cpuTempCaption;
+ tempStr = `${coreType}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()}`;
+ }
+ cpuTemps.push(tempStr);
+ }
+ } catch (e) { /*_*/ }
+ });
+ if(cpuTemps.length > 0) {
+ temps.push(cpuTemps);
+ }
+ });
+ let result = '';
+ temps.forEach((cpuTemps, cpuIndex) => {
+ const strCoreTemps = cpuTemps.map((strTemp, index, arr) => { return strTemp + (index + 1 < arr.length ? (itemsPerRow > 0 && (index + 1) % itemsPerRow === 0 ? '
' : ' | ') : ''); })
+ if(strCoreTemps.length > 0) {
+ result += (cpuCount > 1 ? `CPU ${cpuIndex+1}: ` : '') + strCoreTemps.join('') + (cpuIndex < cpuCount ? '
' : '');
+ }
+ });
+ return '' + (result.length > 0 ? result : 'N/A') + '
';
+ }
+ },
+EOF
+ )
+ #endregion cpu widget heredoc
+ if [[ $? -ne 0 ]]; then
+ echo "Error: Failed to generate cpu widget code" >&2
+ exit 1
+ fi
+}
+
+# Function to generate UPS widget
+generate_nvme_widget() {
+ #region nvme widget heredoc
+ # use subshell to allow variable expansion
+ (
+ export HELPERCTORPARAMS
+ export NVME_ITEMS_PER_ROW
+ cat <<'EOF' | envsubst '$HELPERCTORPARAMS $NVME_ITEMS_PER_ROW' > "$1"
+ {
+ itemId: 'thermalNvme',
+ colspan: 2,
+ printBar: false,
+ title: gettext('NVMe Thermal State'),
+ iconCls: 'fa fa-fw fa-thermometer-half',
+ textField: 'sensorsOutput',
+ renderer: function(value) {
+ // sensors configuration
+ const addressPrefix = "nvme-pci-";
+ const sensorName = "Composite";
+ const tempHelper = Ext.create('PVE.mod.TempHelper', $HELPERCTORPARAMS);
+ // display configuration
+ const itemsPerRow = $NVME_ITEMS_PER_ROW;
+ // ---
+ let objValue;
+ try {
+ objValue = JSON.parse(value) || {};
+ } catch(e) {
+ objValue = {};
+ }
+ const nvmeKeys = Object.keys(objValue).filter(item => String(item).startsWith(addressPrefix)).sort();
+ let temps = [];
+ nvmeKeys.forEach((nvmeKey, index) => {
+ try {
+ let tempVal = NaN, tempMax = NaN, tempCrit = NaN;
+ Object.keys(objValue[nvmeKey][sensorName]).forEach((secondLevelKey) => {
+ if (secondLevelKey.endsWith('_input')) {
+ tempVal = tempHelper.getTemp(parseFloat(objValue[nvmeKey][sensorName][secondLevelKey]));
+ } else if (secondLevelKey.endsWith('_max')) {
+ tempMax = tempHelper.getTemp(parseFloat(objValue[nvmeKey][sensorName][secondLevelKey]));
+ } else if (secondLevelKey.endsWith('_crit')) {
+ tempCrit = tempHelper.getTemp(parseFloat(objValue[nvmeKey][sensorName][secondLevelKey]));
+ }
+ });
+ if (!isNaN(tempVal)) {
+ let tempStyle = '';
+ if (!isNaN(tempMax) && tempVal >= tempMax) {
+ tempStyle = 'color: #FFC300; font-weight: bold;';
+ }
+ if (!isNaN(tempCrit) && tempVal >= tempCrit) {
+ tempStyle = 'color: red; font-weight: bold;';
+ }
+ const tempStr = `Drive ${index + 1}: ${Ext.util.Format.number(tempVal, '0.0')}${tempHelper.getUnit()}`;
+ temps.push(tempStr);
+ }
+ } catch(e) { /*_*/ }
+ });
+ const result = temps.map((strTemp, index, arr) => { return strTemp + (index + 1 < arr.length ? ((index + 1) % itemsPerRow === 0 ? '
' : ' | ') : ''); });
+ return '' + (result.length > 0 ? result.join('') : 'N/A') + '
';
+ }
+ },
+EOF
+ )
+ #endregion nvme widget heredoc
+ if [[ $? -ne 0 ]]; then
+ echo "Error: Failed to generate nvme widget code" >&2
+ exit 1
+ fi
+}
+
+# Function to generate UPS widget
+generate_fan_widget() {
+ #region fan widget heredoc
+ # use subshell to allow variable expansion
+ (
+ export DISPLAY_ZERO_SPEED_FANS
+ cat <<'EOF' | envsubst '$DISPLAY_ZERO_SPEED_FANS' > "$1"
+ {
+ xtype: 'box',
+ colspan: 2,
+ html: gettext('Cooling'),
+ },
+ {
+ itemId: 'speedFan',
+ colspan: 2,
+ printBar: false,
+ title: gettext('Fan Speed(s)'),
+ iconCls: 'fa fa-fw fa-snowflake-o',
+ textField: 'sensorsOutput',
+ renderer: function(value) {
+ // ---
+ let objValue;
+ try {
+ objValue = JSON.parse(value) || {};
+ } catch(e) {
+ objValue = {};
+ }
+
+ // Recursive function to find fan keys and values
+ function findFanKeys(obj, fanKeys, parentKey = null) {
+ Object.keys(obj).forEach(key => {
+ const value = obj[key];
+ if (typeof value === 'object' && value !== null) {
+ // If the value is an object, recursively call the function
+ findFanKeys(value, fanKeys, key);
+ } else if (/^fan[0-9]+(_input)?$/.test(key)) {
+ if ($DISPLAY_ZERO_SPEED_FANS != true && value === 0) {
+ // Skip this fan if DISPLAY_ZERO_SPEED_FANS is false and value is 0
+ return;
+ }
+ // If the key matches the pattern, add the parent key and value to the fanKeys array
+ fanKeys.push({ key: parentKey, value: value });
+ }
+ });
+ }
+
+ let speeds = [];
+ // Loop through the parent keys
+ Object.keys(objValue).forEach(parentKey => {
+ const parentObj = objValue[parentKey];
+ // Array to store fan keys and values
+ const fanKeys = [];
+ // Call the recursive function to find fan keys and values
+ findFanKeys(parentObj, fanKeys);
+ // Sort the fan keys
+ fanKeys.sort();
+ // Process each fan key and value
+ fanKeys.forEach(({ key: fanKey, value: fanSpeed }) => {
+ try {
+ const fan = fanKey.charAt(0).toUpperCase() + fanKey.slice(1); // Capitalize the first letter of fanKey
+ speeds.push(`${fan}: ${fanSpeed} RPM`);
+ } catch(e) {
+ console.error(`Error retrieving fan speed for ${fanKey} in ${parentKey}:`, e); // Debug: Log specific error
+ }
+ });
+ });
+ return '' + (speeds.length > 0 ? speeds.join(' | ') : 'N/A') + '
';
+ }
+ },
+EOF
+ )
+ #endregion fan widget heredoc
+ if [[ $? -ne 0 ]]; then
+ echo "Error: Failed to generate fan widget code" >&2
+ exit 1
+ fi
+}
+
+# Function to generate UPS widget
+generate_hdd_widget() {
+ #region hdd widget heredoc
+ # use subshell to allow variable expansion
+ (
+ export HELPERCTORPARAMS
+ export HDD_ITEMS_PER_ROW
+ cat <<'EOF' | envsubst '$HDD_ITEMS_PER_ROW $HELPERCTORPARAMS' > "$1"
+ {
+ itemId: 'thermalHdd',
+ colspan: 2,
+ printBar: false,
+ title: gettext('HDD/SSD Thermal State'),
+ iconCls: 'fa fa-fw fa-thermometer-half',
+ textField: 'sensorsOutput',
+ renderer: function(value) {
+ // sensors configuration
+ const addressPrefix = "drivetemp-scsi-";
+ const sensorName = "temp1";
+ const tempHelper = Ext.create('PVE.mod.TempHelper', $HELPERCTORPARAMS);
+ // display configuration
+ const itemsPerRow = $HDD_ITEMS_PER_ROW;
+ // ---
+ let objValue;
+ try {
+ objValue = JSON.parse(value) || {};
+ } catch(e) {
+ objValue = {};
+ }
+ const drvKeys = Object.keys(objValue).filter(item => String(item).startsWith(addressPrefix)).sort();
+ let temps = [];
+ drvKeys.forEach((drvKey, index) => {
+ try {
+ let tempVal = NaN, tempMax = NaN, tempCrit = NaN;
+ Object.keys(objValue[drvKey][sensorName]).forEach((secondLevelKey) => {
+ if (secondLevelKey.endsWith('_input')) {
+ tempVal = tempHelper.getTemp(parseFloat(objValue[drvKey][sensorName][secondLevelKey]));
+ } else if (secondLevelKey.endsWith('_max')) {
+ tempMax = tempHelper.getTemp(parseFloat(objValue[drvKey][sensorName][secondLevelKey]));
+ } else if (secondLevelKey.endsWith('_crit')) {
+ tempCrit = tempHelper.getTemp(parseFloat(objValue[drvKey][sensorName][secondLevelKey]));
+ }
+ });
+ if (!isNaN(tempVal)) {
+ let tempStyle = '';
+ if (!isNaN(tempMax) && tempVal >= tempMax) {
+ tempStyle = 'color: #FFC300; font-weight: bold;';
+ }
+ if (!isNaN(tempCrit) && tempVal >= tempCrit) {
+ tempStyle = 'color: red; font-weight: bold;';
+ }
+ const tempStr = `Drive ${index + 1}: ${Ext.util.Format.number(tempVal, '0.0')}${tempHelper.getUnit()}`;
+ temps.push(tempStr);
+ }
+ } catch(e) { /*_*/ }
+ });
+ const result = temps.map((strTemp, index, arr) => { return strTemp + (index + 1 < arr.length ? ((index + 1) % itemsPerRow === 0 ? '
' : ' | ') : ''); });
+ return '' + (result.length > 0 ? result.join('') : 'N/A') + '
';
+ }
+ },
+EOF
+ )
+ #endregion hdd widget heredoc
+ if [[ $? -ne 0 ]]; then
+ echo "Error: Failed to generate hhd widget code" >&2
+ exit 1
+ fi
+}
+
+# Function to generate RAM widget
+generate_ram_widget() {
+ #region ram widget heredoc
+ # use subshell to allow variable expansion
+ (
+ export HELPERCTORPARAMS
+ cat <<'EOF' | envsubst '$HELPERCTORPARAMS' > "$1"
+ {
+ xtype: 'box',
+ colspan: 2,
+ html: gettext('RAM'),
+ },
+ {
+ itemId: 'thermalRam',
+ colspan: 2,
+ printBar: false,
+ title: gettext('Thermal State'),
+ iconCls: 'fa fa-fw fa-thermometer-half',
+ textField: 'sensorsOutput',
+ renderer: function(value) {
+ const cpuTempHelper = Ext.create('PVE.mod.TempHelper', $HELPERCTORPARAMS);
+ // Make SODIMM unique keys
+ value = value.split('\n'); // Split by newlines
+ for (let i = 0; i < value.length; i++) {
+ // Check if the current line contains 'SODIMM'
+ if (value[i].includes('SODIMM') && i + 1 < value.length) {
+ // Extract the number '3' following 'temp' from the next line (e.g., "temp3_input": 25.000)
+ let nextLine = value[i + 1];
+ let match = nextLine.match(/"temp(\d+)_input": (\d+\.\d+)/);
+
+ if (match) {
+ let number = match[1]; // Extracted number
+ // Replace the current line with SODIMM by the extracted number
+ value[i] = value[i].replace('SODIMM', `SODIMM${number}`);
+ }
+ }
+ }
+ value = value.join('\n'); // Reverse line split
+
+ let objValue;
+ try {
+ objValue = JSON.parse(value) || {};
+ } catch(e) {
+ objValue = {};
+ }
+
+ // Recursive function to find ram keys and values
+ function findRamKeys(obj, ramKeys, parentKey = null) {
+ Object.keys(obj).forEach(key => {
+ const value = obj[key];
+ if (typeof value === 'object' && value !== null) {
+ // If the value is an object, recursively call the function
+ findRamKeys(value, ramKeys, key);
+ } else if (/^temp\d+_input$/.test(key) && parentKey && parentKey.startsWith("SODIMM")) {
+ if (value !== 0) {
+ ramKeys.push({ key: parentKey, value: value});
+ }
+ }
+ });
+ }
+
+ let ramTemps = [];
+ // Loop through the parent keys
+ Object.keys(objValue).forEach(parentKey => {
+ const parentObj = objValue[parentKey];
+ // Array to store ram keys and values
+ const ramKeys = [];
+ // Call the recursive function to find ram keys and values
+ findRamKeys(parentObj, ramKeys);
+ // Sort the ramKeys keys
+ ramKeys.sort();
+ // Process each ram key and value
+ ramKeys.forEach(({ key: ramKey, value: ramTemp }) => {
+ try {
+ ram = ramKey.replace('SODIMM', 'SODIMM ');
+ ramTemps.push(`${ram}: ${ramTemp}${cpuTempHelper.getUnit()}`);
+ } catch(e) {
+ console.error(`Error retrieving Ram Temp for ${ramTemps} in ${parentKey}:`, e); // Debug: Log specific error
+ }
+ });
+ });
+ return '' + (ramTemps.length > 0 ? ramTemps.join(' | ') : 'N/A') + '
';
+ }
+ },
+EOF
+ )
+ #endregion ram widget heredoc
+ if [[ $? -ne 0 ]]; then
+ echo "Error: Failed to generate ram widget code" >&2
+ exit 1
+ fi
+}
+
+# Function to generate UPS widget
generate_ups_widget() {
#region UPS widget heredoc
cat > "$1" <<'EOF'
- {
- xtype: 'box',
- colspan: 2,
- html: gettext('UPS'),
- },
- {
- itemId: 'upsc',
- colspan: 2,
- printBar: false,
- title: gettext('Device'),
- iconCls: 'fa fa-fw fa-battery-three-quarters',
- textField: 'upsc',
- renderer: function(value) {
- let objValue = {};
- try {
- // Parse the UPS data
- if (typeof value === 'string') {
- const lines = value.split('\n');
- lines.forEach(line => {
- const colonIndex = line.indexOf(':');
- if (colonIndex > 0) {
- const key = line.substring(0, colonIndex).trim();
- const val = line.substring(colonIndex + 1).trim();
- objValue[key] = val;
- }
- });
- } else if (typeof value === 'object') {
- objValue = value || {};
+ {
+ xtype: 'box',
+ colspan: 2,
+ html: gettext('UPS'),
+ },
+ {
+ itemId: 'upsc',
+ colspan: 2,
+ printBar: false,
+ title: gettext('Device'),
+ iconCls: 'fa fa-fw fa-battery-three-quarters',
+ textField: 'upsc',
+ renderer: function(value) {
+ let objValue = {};
+ try {
+ // Parse the UPS data
+ if (typeof value === 'string') {
+ const lines = value.split('\n');
+ lines.forEach(line => {
+ const colonIndex = line.indexOf(':');
+ if (colonIndex > 0) {
+ const key = line.substring(0, colonIndex).trim();
+ const val = line.substring(colonIndex + 1).trim();
+ objValue[key] = val;
+ }
+ });
+ } else if (typeof value === 'object') {
+ objValue = value || {};
+ }
+ } catch(e) {
+ objValue = {};
}
- } catch(e) {
- objValue = {};
- }
- // If objValue is null or empty, return N/A
- if (!objValue || Object.keys(objValue).length === 0) {
- return 'N/A
';
- }
+ // If objValue is null or empty, return N/A
+ if (!objValue || Object.keys(objValue).length === 0) {
+ return 'N/A
';
+ }
- // Helper function to get status color
- function getStatusColor(status) {
- if (!status) return '#999';
- const statusUpper = status.toUpperCase();
- if (statusUpper.includes('OL')) return 'white'; // White for online
- if (statusUpper.includes('OB')) return '#d9534f'; // Red for on battery
- if (statusUpper.includes('LB')) return '#d9534f'; // Red for low battery
- return '#f0ad4e'; // Orange for other states
- }
+ // Helper function to get status color
+ function getStatusColor(status) {
+ if (!status) return '#999';
+ const statusUpper = status.toUpperCase();
+ if (statusUpper.includes('OL')) return 'white'; // White for online
+ if (statusUpper.includes('OB')) return '#d9534f'; // Red for on battery
+ if (statusUpper.includes('LB')) return '#d9534f'; // Red for low battery
+ return '#f0ad4e'; // Orange for other states
+ }
- // Helper function to get load/charge color
- function getPercentageColor(value, isLoad = false) {
- if (!value || isNaN(value)) return '#999';
- const num = parseFloat(value);
- if (isLoad) {
- if (num >= 80) return '#d9534f'; // Red for high load
- if (num >= 60) return '#f0ad4e'; // Orange for medium load
- return 'white'; // White for low load
+ // Helper function to get load/charge color
+ function getPercentageColor(value, isLoad = false) {
+ if (!value || isNaN(value)) return '#999';
+ const num = parseFloat(value);
+ if (isLoad) {
+ if (num >= 80) return '#d9534f'; // Red for high load
+ if (num >= 60) return '#f0ad4e'; // Orange for medium load
+ return 'white'; // White for low load
+ } else {
+ // For battery charge
+ if (num <= 20) return '#d9534f'; // Red for low charge
+ if (num <= 50) return '#f0ad4e'; // Orange for medium charge
+ return 'white'; // White for good charge
+ }
+ }
+
+ // Helper function to format runtime
+ function formatRuntime(seconds) {
+ if (!seconds || isNaN(seconds)) return 'N/A';
+ const mins = Math.floor(seconds / 60);
+ const secs = seconds % 60;
+ return `${mins}m ${secs}s`;
+ }
+
+ // Extract key UPS information
+ const batteryCharge = objValue['battery.charge'];
+ const batteryRuntime = objValue['battery.runtime'];
+ const inputVoltage = objValue['input.voltage'];
+ const upsLoad = objValue['ups.load'];
+ const upsStatus = objValue['ups.status'];
+ const upsModel = objValue['ups.model'] || objValue['device.model'];
+ const testResult = objValue['ups.test.result'];
+ const batteryChargeLow = objValue['battery.charge.low'];
+ const batteryRuntimeLow = objValue['battery.runtime.low'];
+ const upsRealPowerNominal = objValue['ups.realpower.nominal'];
+ const batteryMfrDate = objValue['battery.mfr.date'];
+
+ // Build the status display
+ let displayItems = [];
+
+ // First line: Model info
+ let modelLine = '';
+ if (upsModel) {
+ modelLine = `${upsModel}`;
} else {
- // For battery charge
- if (num <= 20) return '#d9534f'; // Red for low charge
- if (num <= 50) return '#f0ad4e'; // Orange for medium charge
- return 'white'; // White for good charge
+ modelLine = `N/A`;
}
- }
+ displayItems.push(modelLine);
- // Helper function to format runtime
- function formatRuntime(seconds) {
- if (!seconds || isNaN(seconds)) return 'N/A';
- const mins = Math.floor(seconds / 60);
- const secs = seconds % 60;
- return `${mins}m ${secs}s`;
- }
+ // Main status line with all metrics
+ let statusLine = '';
- // Extract key UPS information
- const batteryCharge = objValue['battery.charge'];
- const batteryRuntime = objValue['battery.runtime'];
- const inputVoltage = objValue['input.voltage'];
- const upsLoad = objValue['ups.load'];
- const upsStatus = objValue['ups.status'];
- const upsModel = objValue['ups.model'] || objValue['device.model'];
- const testResult = objValue['ups.test.result'];
- const batteryChargeLow = objValue['battery.charge.low'];
- const batteryRuntimeLow = objValue['battery.runtime.low'];
- const upsRealPowerNominal = objValue['ups.realpower.nominal'];
- const batteryMfrDate = objValue['battery.mfr.date'];
+ // Status
+ if (upsStatus) {
+ const statusUpper = upsStatus.toUpperCase();
+ let statusText = 'Unknown';
+ let statusColor = '#f0ad4e';
- // Build the status display
- let displayItems = [];
+ if (statusUpper.includes('OL')) {
+ statusText = 'Online';
+ statusColor = 'white'; // White for good status
+ } else if (statusUpper.includes('OB')) {
+ statusText = 'On Battery';
+ statusColor = '#d9534f'; // Red for on battery
+ } else if (statusUpper.includes('LB')) {
+ statusText = 'Low Battery';
+ statusColor = '#d9534f'; // Red for low battery
+ } else {
+ statusText = upsStatus;
+ statusColor = '#f0ad4e'; // Orange for unknown status
+ }
- // First line: Model info
- let modelLine = '';
- if (upsModel) {
- modelLine = `${upsModel}`;
- } else {
- modelLine = `N/A`;
- }
- displayItems.push(modelLine);
-
- // Main status line with all metrics
- let statusLine = '';
-
- // Status
- if (upsStatus) {
- const statusUpper = upsStatus.toUpperCase();
- let statusText = 'Unknown';
- let statusColor = '#f0ad4e';
-
- if (statusUpper.includes('OL')) {
- statusText = 'Online';
- statusColor = 'white'; // White for good status
- } else if (statusUpper.includes('OB')) {
- statusText = 'On Battery';
- statusColor = '#d9534f'; // Red for on battery
- } else if (statusUpper.includes('LB')) {
- statusText = 'Low Battery';
- statusColor = '#d9534f'; // Red for low battery
+ statusLine += `Status: ${statusText}`;
} else {
- statusText = upsStatus;
- statusColor = '#f0ad4e'; // Orange for unknown status
+ statusLine += `Status: N/A`;
}
- statusLine += `Status: ${statusText}`;
- } else {
- statusLine += `Status: N/A`;
- }
-
- // Battery charge
- if (statusLine) statusLine += ' | ';
- if (batteryCharge) {
- const chargeColor = getPercentageColor(batteryCharge, false);
- statusLine += `Battery: ${batteryCharge}%`;
- } else {
- statusLine += `Battery: N/A`;
- }
-
- // Load percentage
- if (statusLine) statusLine += ' | ';
- if (upsLoad) {
- const loadColor = getPercentageColor(upsLoad, true);
- statusLine += `Load: ${upsLoad}%`;
- } else {
- statusLine += `Load: N/A`;
- }
-
- // Runtime
- if (statusLine) statusLine += ' | ';
- if (batteryRuntime) {
- const runtime = parseInt(batteryRuntime);
- const runtimeLowThreshold = batteryRuntimeLow ? parseInt(batteryRuntimeLow) : 600;
- let runtimeColor = 'white';
- if (runtime <= runtimeLowThreshold / 2) runtimeColor = '#d9534f'; // Red if less than half of low threshold
- else if (runtime <= runtimeLowThreshold) runtimeColor = '#f0ad4e'; // Orange if at low threshold
-
- statusLine += `Runtime: ${formatRuntime(runtime)}`;
- } else {
- statusLine += `Runtime: N/A`;
- }
-
- // Input voltage
- if (statusLine) statusLine += ' | ';
- if (inputVoltage) {
- statusLine += `Input: ${parseFloat(inputVoltage).toFixed(0)}V`;
- } else {
- statusLine += `Input: N/A`;
- }
-
- // Calculate actual watt usage
- if (statusLine) statusLine += ' | ';
- let actualWattage = null;
- if (upsLoad && upsRealPowerNominal) {
- const load = parseFloat(upsLoad);
- const nominal = parseFloat(upsRealPowerNominal);
- if (!isNaN(load) && !isNaN(nominal)) {
- actualWattage = Math.round((load / 100) * nominal);
+ // Battery charge
+ if (statusLine) statusLine += ' | ';
+ if (batteryCharge) {
+ const chargeColor = getPercentageColor(batteryCharge, false);
+ statusLine += `Battery: ${batteryCharge}%`;
+ } else {
+ statusLine += `Battery: N/A`;
}
+
+ // Load percentage
+ if (statusLine) statusLine += ' | ';
+ if (upsLoad) {
+ const loadColor = getPercentageColor(upsLoad, true);
+ statusLine += `Load: ${upsLoad}%`;
+ } else {
+ statusLine += `Load: N/A`;
+ }
+
+ // Runtime
+ if (statusLine) statusLine += ' | ';
+ if (batteryRuntime) {
+ const runtime = parseInt(batteryRuntime);
+ const runtimeLowThreshold = batteryRuntimeLow ? parseInt(batteryRuntimeLow) : 600;
+ let runtimeColor = 'white';
+ if (runtime <= runtimeLowThreshold / 2) runtimeColor = '#d9534f'; // Red if less than half of low threshold
+ else if (runtime <= runtimeLowThreshold) runtimeColor = '#f0ad4e'; // Orange if at low threshold
+
+ statusLine += `Runtime: ${formatRuntime(runtime)}`;
+ } else {
+ statusLine += `Runtime: N/A`;
+ }
+
+ // Input voltage
+ if (statusLine) statusLine += ' | ';
+ if (inputVoltage) {
+ statusLine += `Input: ${parseFloat(inputVoltage).toFixed(0)}V`;
+ } else {
+ statusLine += `Input: N/A`;
+ }
+
+ // Calculate actual watt usage
+ if (statusLine) statusLine += ' | ';
+ let actualWattage = null;
+ if (upsLoad && upsRealPowerNominal) {
+ const load = parseFloat(upsLoad);
+ const nominal = parseFloat(upsRealPowerNominal);
+ if (!isNaN(load) && !isNaN(nominal)) {
+ actualWattage = Math.round((load / 100) * nominal);
+ }
+ }
+
+ // Real power (calculated watt usage)
+ if (actualWattage !== null) {
+ statusLine += `Output: ${actualWattage}W`;
+ } else {
+ statusLine += `Output: N/A`;
+ }
+
+ displayItems.push(statusLine);
+
+ // Combined battery and test line
+ let batteryTestLine = '';
+ if (batteryMfrDate) {
+ batteryTestLine += `Battery MFD: ${batteryMfrDate}`;
+ } else {
+ batteryTestLine += `Battery MFD: N/A`;
+ }
+
+ if (testResult && !testResult.toLowerCase().includes('no test')) {
+ const testColor = testResult.toLowerCase().includes('passed') ? 'white' : '#d9534f';
+ batteryTestLine += ` | Test: ${testResult}`;
+ } else {
+ batteryTestLine += ` | Test: N/A`;
+ }
+
+ displayItems.push(batteryTestLine);
+
+ // Format the final output
+ return '' + displayItems.join('
') + '
';
}
-
- // Real power (calculated watt usage)
- if (actualWattage !== null) {
- statusLine += `Output: ${actualWattage}W`;
- } else {
- statusLine += `Output: N/A`;
- }
-
- displayItems.push(statusLine);
-
- // Combined battery and test line
- let batteryTestLine = '';
- if (batteryMfrDate) {
- batteryTestLine += `Battery MFD: ${batteryMfrDate}`;
- } else {
- batteryTestLine += `Battery MFD: N/A`;
- }
-
- if (testResult && !testResult.toLowerCase().includes('no test')) {
- const testColor = testResult.toLowerCase().includes('passed') ? 'white' : '#d9534f';
- batteryTestLine += ` | Test: ${testResult}`;
- } else {
- batteryTestLine += ` | Test: N/A`;
- }
-
- displayItems.push(batteryTestLine);
-
- // Format the final output
- return '' + displayItems.join('
') + '
';
- }
- },
+ },
EOF
#endregion UPS widget heredoc
if [[ $? -ne 0 ]]; then
@@ -1243,6 +1422,8 @@ EOF
fi
}
+#endregion widget generation functions
+
# Function to uninstall the modification
function uninstall_mod {
check_root_privileges
@@ -1282,6 +1463,15 @@ function uninstall_mod {
fi
}
+# Function to check if the modification is installed
+check_mod_installation() {
+ if [[ -n $(grep -F '$res->{sensorsOutput}' "$NODES_PM_FILE") ]] && \
+ [[ -n $(grep -F '$res->{systemInfo}' "$NODES_PM_FILE") ]] && \
+ [[ -n $(grep -E "itemId: 'thermal[[:alnum:]]*'" "$PVE_MANAGER_LIB_JS_FILE") ]]; then
+ err "Mod is already installed. Uninstall existing before installing."
+ fi
+}
+
function restart_proxy {
# Restart pveproxy
msg "\nRestarting PVE proxy..."
@@ -1322,13 +1512,13 @@ function set_backup_directory {
if [[ -z "$BACKUP_DIR" ]]; then
# If not set, use the default backup directory, which is based on the home directory and PVE-MODS
BACKUP_DIR="$HOME/PVE-MODS"
- msg "Using default backup directory: $BACKUP_DIR"
+ info "Using default backup directory: $BACKUP_DIR"
else
# If set, ensure it is a valid directory
if [[ ! -d "$BACKUP_DIR" ]]; then
err "The specified backup directory does not exist: $BACKUP_DIR"
fi
- msg "Using custom backup directory: $BACKUP_DIR"
+ info "Using custom backup directory: $BACKUP_DIR"
fi
}
@@ -1340,9 +1530,9 @@ function create_backup_directory {
mkdir -p "$BACKUP_DIR" 2>/dev/null || {
err "Failed to create backup directory: $BACKUP_DIR. Please check permissions."
}
- msg "Created backup directory: $BACKUP_DIR"
+ info "Created backup directory: $BACKUP_DIR"
else
- msg "Backup directory already exists: $BACKUP_DIR"
+ info "Backup directory already exists: $BACKUP_DIR"
fi
}
@@ -1364,13 +1554,15 @@ function create_file_backup() {
err "Backup verification failed for: $backup_file"
fi
- msg "Created backup: $backup_file"
+ info "Created backup: $backup_file"
}
function perform_backup {
local timestamp
timestamp=$(date +%Y%m%d_%H%M%S)
+ msgb "\n===Creating backups of modified files ==="
+
create_backup_directory
create_file_backup "$NODES_PM_FILE" "$timestamp"
create_file_backup "$PVE_MANAGER_LIB_JS_FILE" "$timestamp"