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"