├── pve-mod-sensors.png ├── pve-mod-all.sh ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── security.md ├── updateallcontainers.sh ├── pve-mod-nag-screen.sh ├── readme.md └── pve-mod-gui-sensors.sh /pve-mod-sensors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meliox/PVE-mods/HEAD/pve-mod-sensors.png -------------------------------------------------------------------------------- /pve-mod-all.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script's working directory 4 | SCRIPT_CWD="$(dirname "$(readlink -f "$0")")" 5 | 6 | MODS="pve-mod-gui-sensors.sh pve-mod-nag-screen.sh" 7 | 8 | ACTION=${1:-install} 9 | for m in $MODS; do 10 | "$SCRIPT_CWD/$m" $ACTION 11 | done 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Sensors output** 27 | If applicable, provide sensor datadump (use the argument save-sensors-data) 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | 22 | **Sensors output** 23 | If applicable, provide sensor datadump (use the argument save-sensors-data) 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # This .gitignore file was automatically created by Microsoft(R) Visual Studio. 3 | ################################################################################ 4 | 5 | /.vs/PVE-mods/FileContentIndex/b0cd8cf0-a894-4523-bdd5-7f3d85072f7f.vsidx 6 | /.vs/PVE-mods/FileContentIndex/c351d5b4-2706-444a-87b2-a910a8873e48.vsidx 7 | /.vs/PVE-mods/FileContentIndex/cd6884c2-daf4-47a9-8bbd-cf8a4f886075.vsidx 8 | /.vs/PVE-mods/FileContentIndex/read.lock 9 | /.vs/PVE-mods/v17 10 | /.vs/PVE-mods/v17/.wsuo 11 | /.vs/slnx.sqlite 12 | /.vs/VSWorkspaceState.json 13 | /.vs/PVE-mods/FileContentIndex/8b60d584-2422-418a-808a-7a62a50425b3.vsidx 14 | /.vs/PVE-mods/FileContentIndex/aecfa7d2-4343-412e-a663-cd485991e27d.vsidx 15 | /.vs/PVE-mods/FileContentIndex/da25d4cb-f11b-4cdb-b231-55efa7695267.vsidx 16 | /.vs/PVE-mods/FileContentIndex 17 | -------------------------------------------------------------------------------- /security.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | These scripts and modifications are designed for use with **Proxmox Virtual Environment (PVE)**. 6 | 7 | ⚠️ **Note:** 8 | - Proxmox upgrades may overwrite modified files. Reinstallation of the mods may be required. 9 | - Functionality on unsupported versions is not guaranteed. 10 | 11 | --- 12 | 13 | ## Security Considerations 14 | 15 | - **Root Access Required**: 16 | - All installation steps must be performed as `root`. 17 | - Normal users do not have access to required system paths. 18 | - Misuse may compromise your Proxmox installation. 19 | - **Backups**: Scripts automatically back up modified system files before applying changes. 20 | - **Network Security**: 21 | - UPS information requires network monitoring; secure communication is recommended. 22 | - Multi-node UPS support assumes identical credentials across nodes, which may pose risks if not properly secured. 23 | 24 | --- 25 | 26 | ## Reporting a Vulnerability 27 | If you discover a security issue in these scripts or modifications, please open an issue. 28 | 29 | --- 30 | 31 | ## Best Practices 32 | - Review code prior to installation 33 | - Always test on a **non-production node** before applying in production. 34 | - Keep a full **system backup and VM/container snapshots** before installing modifications. 35 | - Verify downloaded scripts from the official repo via checksum before execution. 36 | - After upgrades, re-check whether mods are still applied correctly. 37 | - Clear browser cache after applying GUI modifications. 38 | 39 | --- 40 | 41 | ## Disclaimer 42 | These scripts are provided **as-is**, without warranty of any kind. 43 | Use at your own risk. The author(s) are not responsible for data loss, downtime, or security breaches resulting from the use of these modifications. -------------------------------------------------------------------------------- /updateallcontainers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script updates all running Proxmox containers, skipping specified excluded containers, and generates a separate log file for each container. 3 | # The script first updates the Proxmox host system, then iterates through each container, updates the container, and reboots it if necessary. 4 | # Each container's log file is stored in $log_path and the main script log file is named container-upgrade-main.log. 5 | 6 | # Path where logs are saved 7 | log_path="/root/scripts" 8 | 9 | # array of container ids to exclude from updates 10 | exclude_containers=("106") 11 | 12 | # path to programs 13 | pct="/usr/sbin/pct" 14 | 15 | # list of container ids we need to iterate through 16 | containers=$($pct list | tail -n +2 | cut -f1 -d' ') 17 | 18 | #### CODE BELOW ######### 19 | container_main_log_file="${log_path}/container-upgrade-main.log" 20 | 21 | echo "[Info] Updating proxmox containers at $(date)" 22 | echo "[Info] Updating proxmox containers at $(date)" >> $container_main_log_file 23 | 24 | #function to update individual containers 25 | function update_container() { 26 | container=$1 27 | # log file for individual container 28 | container_log_file="${log_path}/container-upgrade-$container.log" 29 | 30 | # log start of update 31 | echo "[Info] Starting update for container $container at $(date)" >> $container_log_file 32 | 33 | # perform the update 34 | $pct exec $container -- bash -c "apt update && apt upgrade -y && apt autoremove -y && reboot" >> $container_log_file 2>&1 35 | 36 | # log completion of update 37 | echo "[Info] Completed update for $container at $(date)" >> $container_log_file 38 | echo "--------------------------------------------------------------------------------------------" >> $container_log_file 39 | } 40 | 41 | for container in $containers; do 42 | # skip excluded containers 43 | if [[ " ${exclude_containers[@]} " =~ " ${container} " ]]; then 44 | echo "[Info] Skipping excluded container, $container" 45 | echo "[Info] Skipping excluded container, $container" >> $container_main_log_file 46 | continue 47 | fi 48 | 49 | status=$($pct status $container) 50 | if [ "$status" == "status: stopped" ]; then 51 | echo "[Info] Skipping offline container, $container" 52 | echo "[Info] Skipping offline container, $container" >> $container_main_log_file 53 | elif [ "$status" == "status: running" ]; then 54 | update_container $container 55 | fi 56 | done; wait 57 | 58 | # log completion of all updates 59 | echo "[Info] Updating proxmox containers completed at $(date)" 60 | echo "[Info] Updating proxmox containers completed at $(date)" >> $container_main_log_file 61 | echo "--------------------------------------------------------------------------------------------" >> $container_main_log_file -------------------------------------------------------------------------------- /pve-mod-nag-screen.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # This bash script installs a modification to the Proxmox Virtual Environment (PVE) web user interface (UI) which deactivates the subscription nag screen. 4 | # 5 | 6 | ################### Configuration ############# 7 | 8 | # This script's working directory 9 | SCRIPT_CWD="$(dirname "$(readlink -f "$0")")" 10 | 11 | # Files backup location 12 | BACKUP_DIR="$SCRIPT_CWD/backup" 13 | 14 | # File paths 15 | proxmoxlibjs="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js" 16 | proxmoxlibminjs="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js" 17 | 18 | ############################################### 19 | 20 | # Helper functions 21 | function msg { 22 | echo -e "\e[0m$1\e[0m" 23 | } 24 | 25 | #echo message in bold 26 | function msgb { 27 | echo -e "\e[1m$1\e[0m" 28 | } 29 | 30 | function warn { 31 | echo -e "\e[0;33m[warning] $1\e[0m" 32 | } 33 | 34 | function err { 35 | echo -e "\e[0;31m[error] $1\e[0m" 36 | exit 1 37 | } 38 | # End of helper functions 39 | 40 | # Function to display usage information 41 | function usage { 42 | msgb "\nUsage:\n$0 [install | uninstall]\n" 43 | exit 1 44 | } 45 | 46 | function restart_proxy { 47 | # Restart pveproxy 48 | msg "\nRestarting PVE proxy..." 49 | systemctl restart pveproxy 50 | } 51 | 52 | function install_mod { 53 | msg "\nPreparing mod installation..." 54 | 55 | local timestamp=$(date '+%Y-%m-%d_%H-%M-%S') 56 | local restart=false 57 | 58 | if (! grep -q "// disable subscription nag screen" "$proxmoxlibjs"); then 59 | if [ -f "$proxmoxlibjs" ]; then 60 | mkdir -p "$BACKUP_DIR" || err "Error creating backup directory." 61 | 62 | # Create backup of original file 63 | msg "Saving current version of \"$proxmoxlibjs\" to \"$BACKUP_DIR/proxmoxlib.js.$timestamp\"." 64 | cp -P "$proxmoxlibjs" "$BACKUP_DIR/proxmoxlib.js.$timestamp" || err "Error creating backup." 65 | 66 | msg "Deactivating the nag screen..." 67 | sed -i "/Ext.define('Proxmox.Utils',/ { 68 | :a; 69 | /checked_command:\s*function\s*(orig_cmd)\s*{/!{N;ba;} 70 | a\ 71 | \\ 72 | // disable subscription nag screen\n\ 73 | orig_cmd();\n\ 74 | return; 75 | }" "$proxmoxlibjs" 76 | fi 77 | restart=true 78 | else 79 | warn "Nag screen already deactivated." 80 | fi 81 | 82 | if [ ! -h "$proxmoxlibminjs" ]; then 83 | msg "Disabling minified front-end library file..." 84 | (mv "$proxmoxlibminjs" "$BACKUP_DIR/proxmoxlib.min.js.$timestamp" && 85 | ln -s "$proxmoxlibjs" "$proxmoxlibminjs") || err "Error disabling minified front-end library file." 86 | restart=true 87 | else 88 | warn "Minified front-end library file already disabled." 89 | fi 90 | 91 | if [ $restart = true ]; then 92 | restart_proxy 93 | fi 94 | } 95 | 96 | function uninstall_mod { 97 | msg "\nRestoring modified files..." 98 | 99 | local restart=false 100 | 101 | # Find the latest backup file of proxmoxlib.js 102 | local latest_proxmoxlibjs=$(find "$BACKUP_DIR" -name "proxmoxlib.js.*" -type f -printf '%T+ %p\n' 2>/dev/null | sort -r | head -n 1 | awk '{print $2}') 103 | 104 | if [ -z "$latest_proxmoxlibjs" ]; then 105 | warn "No proxmoxlib.js backup files found." 106 | else 107 | # Remove the latest proxmoxlib.js file 108 | msg "Restoring \"$proxmoxlibjs\" from the latest backup file." 109 | cp "$latest_proxmoxlibjs" "$proxmoxlibjs" 110 | restart=true 111 | fi 112 | 113 | local latest_proxmoxlibminjs=$(find "$BACKUP_DIR" -name "proxmoxlib.min.js.*" -type f -printf '%T+ %p\n' 2>/dev/null | sort -r | head -n 1 | awk '{print $2}') 114 | 115 | if [ -z "$latest_proxmoxlibminjs" ]; then 116 | warn "No proxmoxlib.min.js backup files found." 117 | else 118 | # Remove the latest proxmoxlib.min.js file 119 | msg "Restoring \"$proxmoxlibminjs\" from the latest backup file." 120 | rm "$proxmoxlibminjs" && cp "$latest_proxmoxlibminjs" "$proxmoxlibminjs" 121 | restart=true 122 | fi 123 | 124 | if [ $restart = true ]; then 125 | restart_proxy 126 | fi 127 | } 128 | 129 | # Process the arguments using a while loop and a case statement 130 | executed=0 131 | while [[ $# -gt 0 ]]; do 132 | case "$1" in 133 | install) 134 | executed=$(($executed + 1)) 135 | msgb "\nInstalling the Proxmox VE nag screen mod..." 136 | install_mod 137 | echo # add a new line 138 | ;; 139 | uninstall) 140 | executed=$(($executed + 1)) 141 | msgb "\nUninstalling the Proxmox VE nag screen mod..." 142 | uninstall_mod 143 | echo # add a new line 144 | ;; 145 | esac 146 | shift 147 | done 148 | 149 | # If no arguments were provided or all arguments have been processed, print the usage message 150 | if [[ $executed -eq 0 ]]; then 151 | usage 152 | fi 153 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Proxmox Virtual Environment mods and scripts 2 | A small collection of scripts and mods for Proxmox Virtual Environment (PVE) 3 | 4 | If you find this helpful, a small donation is appreciated, [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=K8XPMSEBERH3W). 5 | 6 | ## Node sensor readings view 7 | (Tested compatibility: 9.x. Using older version (7.x-8.x), use git version from Apr 6th 2025) 8 | ![Promxox temp mod](https://github.com/Meliox/PVE-mods/blob/main/pve-mod-sensors.png?raw=true) 9 | 10 | This bash script installs a modification to the Proxmox Virtual Environment (PVE) web user interface (UI) to display sensor readings in a flexible and readable manner. 11 | The following readings are possible: 12 | - CPU, NVMe/HDD/SSD temperatures (Celsius/Fahrenheit), fan speeds, ram temperatures via lm-sensors. Note: Hdds require kernel module *drivetemp* module installed. 13 | - UPS information via Network Monitoring Tool 14 | - Motherboard information or system information via dmidecode 15 | 16 | ### How it works 17 | The modification involves the following steps: 18 | 1. Backup original files in the home directory/backup 19 | - `/usr/share/pve-manager/js/pvemanagerlib.js` 20 | - `/usr/share/perl5/PVE/API2/Nodes.pm` 21 | 2. Patch `Nodes.pm` to enable readings. 22 | 3. Modify `pvemanagerlib.js` to: 23 | - Expand the node status view to full browser width. 24 | - Add reading (depending on & selections). 25 | - Allow collapsing the panel vertically. 26 | 4. Restart the `pveproxy` service to apply changes. 27 | 28 | The script provides three options: 29 | | **Option** | **Description** | 30 | |-------------------------|-----------------------------------------------------------------------------| 31 | | `install` | Apply the modification. | 32 | | `uninstall` | Restore original files from backups. | 33 | | `save-sensors-data` | Save a local copy of detected sensor data for reference or troubleshooting. | 34 | 35 | Notes: 36 | - UPS support in multi-node setups require identical login credentials across nodes. This has not been fully tested. 37 | - Proxmox upgrades may overwrite modified files; reinstallation of this mod could be required. 38 | 39 | ### Install 40 | Instructions be performed as 'root', as normal users do not have access to the files. 41 | 42 | ``` 43 | apt-get install lm-sensors 44 | # lm-sensors must be configured, run below to configure your sensors, apply temperature offsets. Refer to lm-sensors manual for more information. 45 | sensors-detect 46 | wget https://raw.githubusercontent.com/Meliox/PVE-mods/refs/heads/main/pve-mod-gui-sensors.sh 47 | bash pve-mod-gui-sensors.sh install 48 | # Then clear the browser cache to ensure all changes are visualized. 49 | ``` 50 | Additionally, adjustments are available in the first part of the script, where paths can be edited, cpucore offset and display information. 51 | 52 | ## Nag screen deactivation 53 | (Tested compatibility: 7.x - 8.3.5) 54 | This bash script installs a modification to the Proxmox Virtual Environment (PVE) web user interface (UI) which deactivates the subscription nag screen. 55 | 56 | The modification includes two main steps: 57 | 1. Create backups of the original files in the `backup` directory relative to the script location. 58 | 2. Modify code. 59 | 60 | The script provides three options: 61 | | **Option** | **Description** | 62 | |-------------------------|-----------------------------------------------------------------------------| 63 | | `install` | Installs the modification by applying the necessary changes. | 64 | | `uninstall` | Removes the modification by restoring the original files from backups. | 65 | 66 | ### Install 67 | Instructions be performed as 'root', as normal users do not have access to the files. 68 | ``` 69 | wget https://raw.githubusercontent.com/Meliox/PVE-mods/refs/heads/main/pve-mod-nag-screen.sh 70 | bash pve-mod-nag-screen.sh install 71 | ``` 72 | 73 | ## Script to update all containers 74 | (Tested compatibility: 7.x - 8.3.5) 75 | 76 | This script updates all running Proxmox containers, skipping specified excluded containers, and generates a separate log file for each container. 77 | The script first updates the Proxmox host system, then iterates through each container, updates the container, and reboots it if necessary. 78 | Each container's log file is stored in $log_path and the main script log file is named container-upgrade-main.log. 79 | 80 | ### Install 81 | ``` 82 | wget https://raw.githubusercontent.com/Meliox/PVE-mods/refs/heads/main/updateallcontainers.sh 83 | ``` 84 | Or use git clone. 85 | Can be added to cron for e.g. monthly update: ```0 6 1 * * /root/scripts/updateallcontainers.sh``` 86 | -------------------------------------------------------------------------------- /pve-mod-gui-sensors.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # This bash script installs a modification to the Proxmox Virtual Environment (PVE) web user interface (UI) to display sensors information. 4 | # 5 | 6 | ################### Configuration ############# 7 | 8 | # Display configuration for HDD, NVME, CPU 9 | # Set to 0 to disable line breaks 10 | # Note: use these settings only if the displayed layout is broken 11 | CPU_ITEMS_PER_ROW=0 12 | NVME_ITEMS_PER_ROW=0 13 | HDD_ITEMS_PER_ROW=0 14 | 15 | # Known CPU sensor names. They can be full or partial but should ensure unambiguous identification. 16 | # Should new ones be added, also update logic in configure() function. 17 | KNOWN_CPU_SENSORS=("coretemp-isa-" "k10temp-pci-") 18 | 19 | # Overwrite default backup location 20 | BACKUP_DIR="" 21 | 22 | ##################### DO NOT EDIT BELOW ####################### 23 | # Only to be used to debug on other systems. Save the "sensor -j" output into a json file. 24 | # Information will be loaded for script configuration and presented in Proxmox. 25 | 26 | # DEV NOTE: lm-sensors version >3.6.0 breakes properly formatted JSON output using 'sensors -j'. This implements a workaround using uses a python3 for formatting 27 | 28 | DEBUG_REMOTE=false 29 | DEBUG_JSON_FILE="/tmp/sensordata.json" 30 | DEBUG_UPS_FILE="/tmp/upsc.txt" 31 | 32 | # This script's working directory 33 | SCRIPT_CWD="$(dirname "$(readlink -f "$0")")" 34 | 35 | # Debug location 36 | JSON_EXPORT_DIRECTORY="$SCRIPT_CWD" 37 | JSON_EXPORT_FILENAME="sensorsdata.json" 38 | 39 | # File paths 40 | PVE_MANAGER_LIB_JS_FILE="/usr/share/pve-manager/js/pvemanagerlib.js" 41 | NODES_PM_FILE="/usr/share/perl5/PVE/API2/Nodes.pm" 42 | 43 | #region message tools 44 | # Section header (bold) 45 | function msgb() { 46 | local message="$1" 47 | echo -e "\e[1m${message}\e[0m" 48 | } 49 | 50 | # Info (green) 51 | function info() { 52 | local message="$1" 53 | echo -e "\e[0;32m[info] ${message}\e[0m" 54 | } 55 | 56 | # Warning (yellow) 57 | function warn() { 58 | local message="$1" 59 | echo -e "\e[0;33m[warning] ${message}\e[0m" 60 | } 61 | 62 | # Error (red) 63 | function err() { 64 | local message="$1" 65 | echo -e "\e[0;31m[error] ${message}\e[0m" 66 | exit 1 67 | } 68 | 69 | # Prompts (cyan or bold) 70 | function ask() { 71 | local prompt="$1" 72 | local response 73 | read -p $'\n\e[1;36m'"${prompt}:"$'\e[0m ' response 74 | echo "$response" 75 | } 76 | #endregion message tools 77 | 78 | # Function to display usage information 79 | function usage { 80 | msgb "\nUsage:\n$0 [install | uninstall | save-sensors-data]\n" 81 | exit 1 82 | } 83 | 84 | # System checks 85 | function check_root_privileges() { 86 | [[ $EUID -eq 0 ]] || err "This script must be run as root. Please run it with 'sudo $0'." 87 | info "Root privileges verified." 88 | } 89 | 90 | # Define a function to install packages 91 | function install_packages { 92 | # Check if the 'sensors' command is available on the system 93 | if (! command -v sensors &>/dev/null); then 94 | # If the 'sensors' command is not available, prompt the user to install lm-sensors 95 | local choiceInstallLmSensors=$(ask "lm-sensors is not installed. Would you like to install it? (y/n)") 96 | case "$choiceInstallLmSensors" in 97 | [yY]) 98 | # If the user chooses to install lm-sensors, update the package list and install the package 99 | apt-get update 100 | apt-get install lm-sensors 101 | ;; 102 | [nN]) 103 | # If the user chooses not to install lm-sensors, exit the script with a zero status code 104 | msgb "Decided to not install lm-sensors. The mod cannot run without it. Exiting..." 105 | err "lm-sensors is required. Exiting..." 106 | ;; 107 | *) 108 | # If the user enters an invalid input, print an error message and exit the script with a non-zero status code 109 | err "Invalid input. Exiting..." 110 | ;; 111 | esac 112 | fi 113 | 114 | # Check if lm-sensors is installed correctly and exit if not 115 | if (! command -v sensors &>/dev/null); then 116 | err "lm-sensors installation failed or 'sensors' command is not available. Please install lm-sensors manually and re-run the script." 117 | fi 118 | } 119 | 120 | function configure { 121 | SENSORS_DETECTED=false 122 | local sensorsOutput 123 | local sanitisedSensorsOutput 124 | local upsOutput 125 | local modelName 126 | 127 | install_packages 128 | 129 | #### Collect lm-sensors output #### 130 | #region sensors collection 131 | if [ "$DEBUG_REMOTE" = true ]; then 132 | warn "Remote debugging is used. Sensor readings from dump file $DEBUG_JSON_FILE will be used." 133 | warn "Remote debugging is used. UPS readings from dump file $DEBUG_UPS_FILE will be used." 134 | sensorsOutput=$(cat "$DEBUG_JSON_FILE") 135 | else 136 | sensorsOutput=$(sensors -j 2>/dev/null) 137 | fi 138 | 139 | # Apply lm-sensors sanitization 140 | sanitisedSensorsOutput=$(sanitize_sensors_output "$sensorsOutput") 141 | 142 | if [ $? -ne 0 ]; then 143 | err "Sensor output error.\n\nCommand output:\n${sanitisedSensorsOutput}\n\nExiting..." 144 | fi 145 | #endregion sensors collection 146 | 147 | #### CPU #### 148 | #region cpu setup 149 | msgb "\n=== Detecting CPU temperature sensors ===" 150 | ENABLE_CPU=false 151 | local cpuList="" 152 | local cpuCount=0 153 | 154 | # Find all CPU sensors that match known patterns 155 | for pattern in "${KNOWN_CPU_SENSORS[@]}"; do 156 | found_sensors=$(echo "$sanitisedSensorsOutput" | grep -o "\"${pattern}[^\"]*\"" | sed 's/"//g') 157 | if [ -n "$found_sensors" ]; then 158 | while read -r sensor; do 159 | if [ -n "$sensor" ]; then 160 | cpuCount=$((cpuCount + 1)) 161 | if [ -z "$cpuList" ]; then 162 | cpuList="$sensor" 163 | else 164 | cpuList="$cpuList,$sensor" 165 | fi 166 | ENABLE_CPU=true 167 | fi 168 | done <<< "$found_sensors" 169 | fi 170 | done 171 | 172 | if [ "$ENABLE_CPU" = true ]; then 173 | info "Detected CPU sensors ($cpuCount): $cpuList" 174 | SENSORS_DETECTED=true 175 | while true; do 176 | local choice=$(ask "Display temperatures for all cores [C] or average per CPU [a] (some newer AMD variants support per die)? (C/a)") 177 | case "$choice" in 178 | [cC]|"") 179 | CPU_TEMP_TARGET="Core" 180 | info "Temperatures will be displayed for all cores." 181 | break 182 | ;; 183 | [aA]) 184 | CPU_TEMP_TARGET="Package" 185 | info "An average temperature will be displayed per CPU." 186 | break 187 | ;; 188 | *) 189 | warn "Invalid input, please choose C or a." 190 | ;; 191 | esac 192 | done 193 | else 194 | warn "No CPU temperature sensors found." 195 | fi 196 | #endregion cpu setup 197 | 198 | #### RAM #### 199 | #region ram setup 200 | msgb "\n=== Detecting RAM temperature sensors ===" 201 | local ramList=$(echo "$sanitisedSensorsOutput" | grep -o '"SODIMM[^"]*"' | sed 's/"//g' | paste -sd, -) 202 | local ramCount=$(grep -c '"SODIMM[^"]*"' <<<"$sanitisedSensorsOutput") 203 | 204 | if [ "$ramCount" -gt 0 ]; then 205 | info "Detected RAM sensors ($ramCount): $ramList" 206 | ENABLE_RAM_TEMP=true 207 | SENSORS_DETECTED=true 208 | else 209 | warn "No RAM temperature sensors found." 210 | ENABLE_RAM_TEMP=false 211 | fi 212 | #endregion ram setup 213 | 214 | #### HDD/SSD #### 215 | #region hdd setup 216 | msgb "\n=== Detecting HDD/SSD temperature sensors ===" 217 | local hddList=($(echo "$sanitisedSensorsOutput" | grep -o '"drivetemp-scsi[^"]*"' | sed 's/"//g')) 218 | if [ ${#hddList[@]} -gt 0 ]; then 219 | info "Detected HDD/SSD sensors (${#hddList[@]}): $(IFS=,; echo "${hddList[*]}")" 220 | ENABLE_HDD_TEMP=true 221 | SENSORS_DETECTED=true 222 | else 223 | warn "No HDD/SSD temperature sensors found." 224 | ENABLE_HDD_TEMP=false 225 | fi 226 | #endregion hdd setup 227 | 228 | #### NVMe #### 229 | #region nvme setup 230 | msgb "\n=== Detecting NVMe temperature sensors ===" 231 | local nvmeList=($(echo "$sanitisedSensorsOutput" | grep -o '"nvme[^"]*"' | sed 's/"//g')) 232 | if [ ${#nvmeList[@]} -gt 0 ]; then 233 | info "Detected NVMe sensors (${#nvmeList[@]}): $(IFS=,; echo "${nvmeList[*]}")" 234 | ENABLE_NVME_TEMP=true 235 | SENSORS_DETECTED=true 236 | else 237 | warn "No NVMe temperature sensors found." 238 | ENABLE_NVME_TEMP=false 239 | fi 240 | #endregion nvme setup 241 | 242 | #### Fans #### 243 | #region fan setup 244 | msgb "\n=== Detecting fan speed sensors ===" 245 | 246 | local fanList="" 247 | local fanCount=0 248 | 249 | # Find all fan names that have fan*_input entries 250 | fanList=$(echo "$sanitisedSensorsOutput" | grep -B2 '"fan[0-9]\+_input"' | grep '".*": {' | sed 's/.*"\([^"]*\)": {.*/\1/' | sort -u | paste -sd, -) 251 | fanCount=$(grep -c 'fan[0-9]\+_input' <<<"$sanitisedSensorsOutput") 252 | 253 | if [ "$fanCount" -gt 0 ]; then 254 | info "Detected fan speed sensors ($fanCount): $fanList" 255 | ENABLE_FAN_SPEED=true 256 | SENSORS_DETECTED=true 257 | 258 | local choice 259 | choice=$(ask "Display fans reporting zero speed? (Y/n)") 260 | case "$choice" in 261 | [yY]|"") 262 | DISPLAY_ZERO_SPEED_FANS=true 263 | info "Zero-speed fans will be displayed." 264 | ;; 265 | [nN]) 266 | DISPLAY_ZERO_SPEED_FANS=false 267 | info "Only active fans will be displayed." 268 | ;; 269 | *) 270 | warn "Invalid input. Defaulting to show zero-speed fans." 271 | DISPLAY_ZERO_SPEED_FANS=true 272 | ;; 273 | esac 274 | else 275 | warn "No fan speed sensors found." 276 | ENABLE_FAN_SPEED=false 277 | fi 278 | #endregion fan setup 279 | 280 | #### Temperature Units #### 281 | #region temp unit setup 282 | msgb "\n=== Display temperature ===" 283 | if [ "$SENSORS_DETECTED" = true ]; then 284 | local unit=$(ask "Display temperatures in Celsius [C] or Fahrenheit [f]? (C/f)") 285 | case "$unit" in 286 | [cC]|"") 287 | TEMP_UNIT="C" 288 | info "Using Celsius." 289 | ;; 290 | [fF]) 291 | TEMP_UNIT="F" 292 | info "Using Fahrenheit." 293 | ;; 294 | *) 295 | warn "Invalid selection. Defaulting to Celsius." 296 | TEMP_UNIT="C" 297 | ;; 298 | esac 299 | fi 300 | #endregion temp unit setup 301 | 302 | #### UPS #### 303 | #region ups setup 304 | local choiceUPS=$(ask "Enable UPS information? (y/N)") 305 | case "$choiceUPS" in 306 | [yY]) 307 | if [ "$DEBUG_REMOTE" = true ]; then 308 | upsOutput=$(cat "$DEBUG_UPS_FILE") 309 | info "Remote debugging: UPS readings from $DEBUG_UPS_FILE" 310 | upsConnection="DEBUG_UPS" 311 | else 312 | upsConnection=$(ask "Enter UPS connection (e.g., upsname[@hostname[:port]])") 313 | if ! command -v upsc &>/dev/null; then 314 | err "The 'upsc' command is not available. Install 'nut-client'." 315 | fi 316 | upsOutput=$(upsc "$upsConnection" 2>&1) 317 | fi 318 | 319 | if echo "$upsOutput" | grep -q "device.model:"; then 320 | modelName=$(echo "$upsOutput" | grep "device.model:" | cut -d':' -f2- | xargs) 321 | ENABLE_UPS=true 322 | info "Connected to UPS model: $modelName at $upsConnection." 323 | else 324 | warn "Failed to connect to UPS at '$upsConnection'." 325 | ENABLE_UPS=false 326 | fi 327 | ;; 328 | [nN]|"") 329 | ENABLE_UPS=false 330 | info "UPS information will not be displayed." 331 | ;; 332 | *) 333 | warn "Invalid selection. UPS info will not be displayed." 334 | ENABLE_UPS=false 335 | ;; 336 | esac 337 | #endregion ups setup 338 | 339 | #### System Info #### 340 | #region system info setup 341 | msgb "\n=== Detecting System Information ===" 342 | for i in 1 2; do 343 | echo "type ${i})" 344 | dmidecode -t "$i" | awk -F': ' '/Manufacturer|Product Name|Serial Number/ {print $1": "$2}' 345 | done 346 | local choiceSysInfo=$(ask "Enable system information? (1/2/n)") 347 | case "$choiceSysInfo" in 348 | [1]|"") 349 | ENABLE_SYSTEM_INFO=true 350 | SYSTEM_INFO_TYPE=1 351 | info "System information will be displayed." 352 | ;; 353 | [2]) 354 | ENABLE_SYSTEM_INFO=true 355 | SYSTEM_INFO_TYPE=2 356 | info "Motherboard information will be displayed." 357 | ;; 358 | [nN]) 359 | ENABLE_SYSTEM_INFO=false 360 | info "System information will NOT be displayed." 361 | ;; 362 | *) 363 | warn "Invalid selection. Defaulting to system information." 364 | ENABLE_SYSTEM_INFO=true 365 | SYSTEM_INFO_TYPE=1 366 | ;; 367 | esac 368 | #endregion system info setup 369 | 370 | #### Final Check #### 371 | #region final check 372 | if [ "$SENSORS_DETECTED" = false ] && [ "$ENABLE_UPS" = false ] && [ "$ENABLE_SYSTEM_INFO" = false ]; then 373 | err "No sensors detected, UPS or system info enabled. Exiting." 374 | fi 375 | #endregion final check 376 | } 377 | 378 | 379 | # Function to install the modification 380 | function install_mod { 381 | local upsConnection 382 | msgb "\n=== Preparing mod installation ===" 383 | check_root_privileges 384 | check_mod_installation 385 | configure 386 | perform_backup 387 | 388 | #### Insert information retrieval code #### 389 | msgb "\n=== Inserting information retrieval code ===" 390 | insert_node_info 391 | 392 | #### Temperature helper parameters #### 393 | msgb "\n=== Creating temperature conversion helper ===" 394 | HELPERCTORPARAMS=$([[ "$TEMP_UNIT" = "F" ]] && \ 395 | echo '{srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: PVE.mod.TempHelper.FAHRENHEIT}' || \ 396 | echo '{srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: PVE.mod.TempHelper.CELSIUS}') 397 | info "Temperature helper configured for $TEMP_UNIT." 398 | 399 | #### Expand StatusView space #### 400 | expand_statusview_space 401 | 402 | #### Insert temperature helper #### 403 | generate_and_insert_temp_helper 404 | 405 | #### Generate and insert widgets #### 406 | msgb "\n=== Making visual adjustments ===" 407 | 408 | generate_and_insert_widget "$ENABLE_SYSTEM_INFO" "generate_system_info" "system_info" 409 | generate_and_insert_widget "$ENABLE_UPS" "generate_ups_widget" "ups" 410 | generate_and_insert_widget "$ENABLE_HDD_TEMP" "generate_hdd_widget" "hdd" 411 | generate_and_insert_widget "$ENABLE_NVME_TEMP" "generate_nvme_widget" "nvme" 412 | 413 | if [[ "$ENABLE_HDD_TEMP" = true || "$ENABLE_NVME_TEMP" = true ]]; then 414 | generate_drive_header 415 | info "Drive headers added." 416 | fi 417 | 418 | generate_and_insert_widget "$ENABLE_FAN_SPEED" "generate_fan_widget" "fan" 419 | generate_and_insert_widget "$ENABLE_RAM_TEMP" "generate_ram_widget" "ram" 420 | generate_and_insert_widget "$ENABLE_CPU" "generate_cpu_widget" "cpu" 421 | 422 | #### Visual separation #### 423 | add_visual_separator 424 | info "Added visual separator for modified items." 425 | 426 | #### Node summary #### 427 | setup_node_summary_container 428 | info "Node summary box moved into its own container." 429 | 430 | msgb "\n=== Finalizing installation ===" 431 | 432 | restart_proxy 433 | info "Installation completed." 434 | ask "Clear the browser cache to ensure all changes are visualized. (any key to continue)" 435 | } 436 | 437 | # Sanitize sensors output to handle common lm-sensors parsing issues 438 | sanitize_sensors_output() { 439 | local input="$1" 440 | 441 | # Pipe the text into Perl: 442 | # -0777 → "slurp mode": read the entire stream as one string so 443 | # regexes can match across line breaks. 444 | # -pe → loop over input, applying the script (-e) and printing. 445 | # Apply python3 json.tool for proper formatting and validation 446 | echo "$input" | perl -0777 -pe ' 447 | # Replace ERROR lines with placeholder values 448 | s/ERROR:.+\s(\w+):\s(.+)/"$1": 0.000,/g; 449 | s/ERROR:.+\s(\w+)!/"$1": 0.000,/g; 450 | 451 | # Remove trailing commas before closing braces 452 | s/,\s*(\})/$1/g; 453 | 454 | # Replace NaN values with null 455 | s/\bNaN\b/null/g; 456 | 457 | # Fix duplicate SODIMM keys - handle both pretty and one-line JSON 458 | s/"SODIMM"\s*:\s*\{\s*"temp(\d+)_input"/"SODIMM $1": {\n "temp$1_input"/g; 459 | 460 | # Fix duplicate fan keys - handle both pretty and one-line JSON 461 | s/"([^"]*Fan[^"]*)"\s*:\s*\{\s*"fan(\d+)_input"/"$1 $2": {\n "fan$2_input"/g; 462 | ' | python3 -m json.tool 2>/dev/null || echo "$input" 463 | } 464 | 465 | #region node info insertion 466 | # Main insertion routine 467 | insert_node_info() { 468 | local output_file="$NODES_PM_FILE" 469 | 470 | collect_sensors_output "$output_file" 471 | 472 | if [[ $ENABLE_UPS == true ]]; then 473 | collect_ups_output "$output_file" 474 | fi 475 | 476 | if [[ $ENABLE_SYSTEM_INFO == true ]]; then 477 | collect_system_info "$output_file" 478 | fi 479 | } 480 | 481 | # Collect lm-sensors data 482 | collect_sensors_output() { 483 | local output_file="$1" 484 | local sensorsCmd 485 | 486 | if [[ $DEBUG_REMOTE == true ]]; then 487 | sensorsCmd="cat \"$DEBUG_JSON_FILE\"" 488 | else 489 | # Note: sensors -f (Fahrenheit) breaks fan speeds 490 | sensorsCmd="sensors -j 2>/dev/null" 491 | fi 492 | 493 | # Remember to reflect this in sanitize_sensors_output() 494 | #region sensors heredoc 495 | sed -i '/my \$dinfo = df('\''\/'\'', 1);/i\ 496 | # Collect sensor data from lm-sensors\ 497 | $res->{sensorsOutput} = `'"$sensorsCmd"'`;\ 498 | \ 499 | # Sanitize JSON output to handle common lm-sensors parsing issues\ 500 | # Replace ERROR lines with placeholder values\ 501 | $res->{sensorsOutput} =~ s/ERROR:.+\\s(\\w+):\\s(.+)/\\"$1\\": 0.000,/g;\ 502 | $res->{sensorsOutput} =~ s/ERROR:.+\\s(\\w+)!/\\"$1\\": 0.000,/g;\ 503 | \ 504 | # Remove trailing commas before closing braces\ 505 | $res->{sensorsOutput} =~ s/,\\s*(\})/$1/g;\ 506 | \ 507 | # Replace NaN values with null for valid JSON\ 508 | $res->{sensorsOutput} =~ s/\\bNaN\\b/null/g;\ 509 | \ 510 | # Fix duplicate SODIMM keys by appending temperature sensor number with a space - handle both pretty and one-line JSON\ 511 | # Example: "SODIMM":{"temp3_input":34.0} becomes "SODIMM 3":{"temp3_input":34.0}\ 512 | $res->{sensorsOutput} =~ s/"SODIMM"\\s*:\\s*\\{\\s*"temp(\\d+)_input"/"SODIMM $1": {\\n "temp$1_input"/g;\ 513 | \ 514 | # Fix duplicate fans keys by appending fan number with a space - handle both pretty and one-line JSON\ 515 | # Example: "Processor Fan":{"fan2_input":1000,...} → "Processor Fan 2":{"fan2_input":1000,...}\ 516 | $res->{sensorsOutput} =~ s/"([^"]+)"\\s*:\\s*\{\\s*"fan(\\d+)_input"/"$1 $2": {\\n "fan$2_input"/g;\ 517 | \ 518 | # Format JSON output properly (workaround for lm-sensors >3.6.0 issues)\ 519 | $res->{sensorsOutput} =~ /^(.*)$/s;\ 520 | $res->{sensorsOutput} = `echo \\Q$1\\E | python3 -m json.tool 2>/dev/null || echo \\Q$1\E`;\ 521 | ' "$NODES_PM_FILE" 522 | #endregion sensors heredoc 523 | info "Sensors' retriever added to \"$output_file\"." 524 | } 525 | 526 | # Collect UPS data 527 | collect_ups_output() { 528 | local output_file="$1" 529 | local ups_cmd 530 | 531 | if [[ $DEBUG_REMOTE == true ]]; then 532 | ups_cmd="cat \"$DEBUG_UPS_FILE\"" 533 | else 534 | ups_cmd="upsc \"$upsConnection\" 2>/dev/null" 535 | fi 536 | 537 | # region ups heredoc 538 | sed -i "/my \$dinfo = df('\/', 1);/i\\ 539 | # Collect UPS status information\\ 540 | sub get_upsc {\\ 541 | my \$cmd = '$ups_cmd';\\ 542 | my \$output = \`\\\$cmd\`;\\ 543 | return \$output;\\ 544 | }\\ 545 | \$res->{upsc} = get_upsc();\\ 546 | " "$NODES_PM_FILE" 547 | # endregion ups heredoc 548 | 549 | info "UPS retriever added to \"$output_file\"." 550 | } 551 | 552 | 553 | # Collect system information 554 | collect_system_info() { 555 | local output_file="$1" 556 | local systemInfoCmd 557 | 558 | systemInfoCmd=$(dmidecode -t "${SYSTEM_INFO_TYPE}" \ 559 | | awk -F': ' '/Manufacturer|Product Name|Serial Number/ {print $1": "$2}' \ 560 | | awk '{$1=$1};1' \ 561 | | sed 's/$/ |/' \ 562 | | paste -sd " " - \ 563 | | sed 's/ |$//') 564 | #region system info heredoc 565 | sed -i "/my \$dinfo = df('\/', 1);/i\\ 566 | # Add system information to response\\ 567 | \$res->{systemInfo} = \"$(echo "$systemInfoCmd")\";\\ 568 | " "$NODES_PM_FILE" 569 | #endregion system info heredoc 570 | info "System information retriever added to \"$output_file\"." 571 | } 572 | #endregion node info insertion 573 | 574 | #region widget generation functions 575 | # Helper function to insert widget after thermal items 576 | insert_widget_after_thermal() { 577 | local widget_file="$1" 578 | sed -i "/^Ext.define('PVE.node.StatusView',/ { 579 | :a 580 | /items:/!{N;ba;} 581 | :b 582 | /'cpus.*},/!{N;bb;} 583 | r $widget_file 584 | }" "$PVE_MANAGER_LIB_JS_FILE" 585 | } 586 | 587 | # Helper function to generate widget and insert it 588 | generate_and_insert_widget() { 589 | local enable_flag="$1" 590 | local generator_func="$2" 591 | local widget_name="$3" 592 | 593 | if [ "$enable_flag" = true ]; then 594 | local temp_js_file="/tmp/${widget_name}_widget.js" 595 | "$generator_func" "$temp_js_file" 596 | insert_widget_after_thermal "$temp_js_file" 597 | rm "$temp_js_file" 598 | info "Inserted $widget_name widget." 599 | fi 600 | } 601 | 602 | # Function to generate drive header 603 | generate_drive_header() { 604 | if [ "$ENABLE_NVME_TEMP" = true ] || [ "$ENABLE_HDD_TEMP" = true ]; then 605 | local temp_js_file="/tmp/drive_header.js" 606 | #region drive header heredoc 607 | cat > "$temp_js_file" <<'EOF' 608 | { 609 | xtype: 'box', 610 | colspan: 2, 611 | html: gettext('Drive(s)'), 612 | }, 613 | EOF 614 | #endregion drive header heredoc 615 | if [[ $? -ne 0 ]]; then 616 | echo "Error: Failed to generate drive header code" >&2 617 | exit 1 618 | fi 619 | 620 | insert_widget_after_thermal "$temp_js_file" 621 | rm "$temp_js_file" 622 | fi 623 | } 624 | 625 | # Function to expand space and modify StatusView properties 626 | expand_statusview_space() { 627 | msgb "\n=== Expanding StatusView space ===" 628 | 629 | # Apply multiple modifications to the StatusView definition 630 | sed -i "/Ext.define('PVE\.node\.StatusView'/,/\},/ { 631 | s/\(bodyPadding:\) '[^']*'/\1 '20 15 20 15'/ 632 | s/height: [0-9]\+/minHeight: 360,\n\tflex: 1,\n\tcollapsible: true,\n\ttitleCollapse: true/ 633 | s/\(tableAttrs:.*$\)/trAttrs: \{ valign: 'top' \},\n\t\1/ 634 | }" "$PVE_MANAGER_LIB_JS_FILE" 635 | 636 | if [[ $? -ne 0 ]]; then 637 | echo "Error: Failed to expand StatusView space" >&2 638 | exit 1 639 | fi 640 | 641 | info "Expanded space in \"$PVE_MANAGER_LIB_JS_FILE\"." 642 | } 643 | 644 | # Function to move node summary into its own container 645 | setup_node_summary_container() { 646 | # Move the node summary box into its own container 647 | local temp_js_file="/tmp/summary_container.js" 648 | #region summary container heredoc 649 | cat > "$temp_js_file" <<'EOF' 650 | { 651 | xtype: 'container', 652 | itemId: 'summarycontainer', 653 | layout: 'column', 654 | minWidth: 700, 655 | defaults: { 656 | minHeight: 350, 657 | padding: 5, 658 | columnWidth: 1, 659 | }, 660 | items: [ 661 | nodeStatus, 662 | ] 663 | }, 664 | EOF 665 | #endregion summary container heredoc 666 | if [[ $? -ne 0 ]]; then 667 | echo "Error: Failed to generate summary container code" >&2 668 | exit 1 669 | fi 670 | 671 | # Insert the new container after finding the nodeStatus and items pattern 672 | sed -i "/^\s*nodeStatus: nodeStatus,/ { 673 | :a 674 | /items: \[/ !{N;ba;} 675 | r $temp_js_file 676 | }" "$PVE_MANAGER_LIB_JS_FILE" 677 | 678 | rm "$temp_js_file" 679 | 680 | # Deactivate the original box instance 681 | sed -i "/^\s*nodeStatus: nodeStatus,/ { 682 | :a 683 | /itemId: 'itemcontainer',/ !{N;ba;} 684 | n; 685 | :b 686 | /nodeStatus,/ !{N;bb;} 687 | s/nodeStatus/\/\/nodeStatus/ 688 | }" "$PVE_MANAGER_LIB_JS_FILE" 689 | 690 | if [[ $? -ne 0 ]]; then 691 | echo "Error: Failed to deactivate original nodeStatus instance" >&2 692 | exit 1 693 | fi 694 | } 695 | 696 | # Function to add visual spacing separator after the last widget 697 | add_visual_separator() { 698 | # Check for the presence of items in the reverse order of display 699 | local lastItemId="" 700 | 701 | if [ "$ENABLE_UPS" = true ]; then 702 | lastItemId="upsc" 703 | elif [ "$ENABLE_HDD_TEMP" = true ]; then 704 | lastItemId="thermalHdd" 705 | elif [ "$ENABLE_NVME_TEMP" = true ]; then 706 | lastItemId="thermalNvme" 707 | elif [ "$ENABLE_FAN_SPEED" = true ]; then 708 | lastItemId="speedFan" 709 | else 710 | lastItemId="thermalCpu" 711 | fi 712 | 713 | if [ -n "$lastItemId" ]; then 714 | local temp_js_file="/tmp/visual_separator.js" 715 | 716 | #region visual spacing heredoc 717 | cat > "$temp_js_file" <<'EOF' 718 | { 719 | xtype: 'box', 720 | colspan: 2, 721 | padding: '0 0 20 0', 722 | }, 723 | EOF 724 | #endregion visual spacing heredoc 725 | if [[ $? -ne 0 ]]; then 726 | echo "Error: Failed to generate visual separator code" >&2 727 | exit 1 728 | fi 729 | 730 | # Insert after the specific lastItemId (different pattern than thermal) 731 | sed -i "/^Ext.define('PVE.node.StatusView',/ { 732 | :a; 733 | /^.*{.*'$lastItemId'.*},/!{N;ba;} 734 | r $temp_js_file 735 | }" "$PVE_MANAGER_LIB_JS_FILE" 736 | 737 | rm "$temp_js_file" 738 | fi 739 | } 740 | 741 | # Function to generate system info widget 742 | generate_system_info() { 743 | #region system info heredoc 744 | cat > "$1" <<'EOF' 745 | { 746 | itemId: 'sysinfo', 747 | colspan: 2, 748 | printBar: false, 749 | title: gettext('System Information'), 750 | textField: 'systemInfo', 751 | renderer: function(value){ 752 | return value; 753 | } 754 | }, 755 | EOF 756 | #endregion system info heredoc 757 | if [[ $? -ne 0 ]]; then 758 | echo "Error: Failed to generate system info code" >&2 759 | exit 1 760 | fi 761 | } 762 | 763 | # Function to generate and insert temperature conversion helper class 764 | generate_and_insert_temp_helper() { 765 | local temp_js_file="/tmp/temp_helper.js" 766 | 767 | msgb "\n=== Inserting temperature helper ===" 768 | 769 | #region temp helper heredoc 770 | cat > "$temp_js_file" <<'EOF' 771 | Ext.define('PVE.mod.TempHelper', { 772 | //singleton: true, 773 | 774 | requires: ['Ext.util.Format'], 775 | 776 | statics: { 777 | CELSIUS: 0, 778 | FAHRENHEIT: 1 779 | }, 780 | 781 | srcUnit: null, 782 | dstUnit: null, 783 | 784 | isValidUnit: function (unit) { 785 | return ( 786 | Ext.isNumber(unit) && (unit === this.self.CELSIUS || unit === this.self.FAHRENHEIT) 787 | ); 788 | }, 789 | 790 | constructor: function (config) { 791 | this.srcUnit = config && this.isValidUnit(config.srcUnit) ? config.srcUnit : this.self.CELSIUS; 792 | this.dstUnit = config && this.isValidUnit(config.dstUnit) ? config.dstUnit : this.self.CELSIUS; 793 | }, 794 | 795 | toFahrenheit: function (tempCelsius) { 796 | return Ext.isNumber(tempCelsius) 797 | ? tempCelsius * 9 / 5 + 32 798 | : NaN; 799 | }, 800 | 801 | toCelsius: function (tempFahrenheit) { 802 | return Ext.isNumber(tempFahrenheit) 803 | ? (tempFahrenheit - 32) * 5 / 9 804 | : NaN; 805 | }, 806 | 807 | getTemp: function (value) { 808 | if (this.srcUnit !== this.dstUnit) { 809 | switch (this.srcUnit) { 810 | case this.self.CELSIUS: 811 | switch (this.dstUnit) { 812 | case this.self.FAHRENHEIT: 813 | return this.toFahrenheit(value); 814 | 815 | default: 816 | Ext.raise({ 817 | msg: 818 | 'Unsupported destination temperature unit: ' + this.dstUnit, 819 | }); 820 | } 821 | case this.self.FAHRENHEIT: 822 | switch (this.dstUnit) { 823 | case this.self.CELSIUS: 824 | return this.toCelsius(value); 825 | 826 | default: 827 | Ext.raise({ 828 | msg: 829 | 'Unsupported destination temperature unit: ' + this.dstUnit, 830 | }); 831 | } 832 | default: 833 | Ext.raise({ 834 | msg: 'Unsupported source temperature unit: ' + this.srcUnit, 835 | }); 836 | } 837 | } else { 838 | return value; 839 | } 840 | }, 841 | 842 | getUnit: function(plainText) { 843 | switch (this.dstUnit) { 844 | case this.self.CELSIUS: 845 | return plainText !== true ? '°C' : '\'C'; 846 | 847 | case this.self.FAHRENHEIT: 848 | return plainText !== true ? '°F' : '\'F'; 849 | 850 | default: 851 | Ext.raise({ 852 | msg: 'Unsupported destination temperature unit: ' + this.srcUnit, 853 | }); 854 | } 855 | }, 856 | }); 857 | EOF 858 | #endregion temp helper heredoc 859 | if [[ $? -ne 0 ]]; then 860 | echo "Error: Failed to generate temp helper code" >&2 861 | exit 1 862 | fi 863 | 864 | sed -i "/^Ext.define('PVE.node.StatusView'/e cat /tmp/temp_helper.js" "$PVE_MANAGER_LIB_JS_FILE" 865 | rm "$temp_js_file" 866 | 867 | info "Temperature helper inserted successfully." 868 | } 869 | 870 | # Function to generate CPU widget 871 | generate_cpu_widget() { 872 | #region cpu widget heredoc 873 | # use subshell to allow variable expansion 874 | ( 875 | export CPU_ITEMS_PER_ROW 876 | export CPU_TEMP_TARGET 877 | export HELPERCTORPARAMS 878 | 879 | cat <<'EOF' | envsubst '$CPU_ITEMS_PER_ROW $CPU_TEMP_TARGET $HELPERCTORPARAMS' > "$1" 880 | { 881 | itemId: 'thermalCpu', 882 | colspan: 2, 883 | printBar: false, 884 | title: gettext('CPU Thermal State'), 885 | iconCls: 'fa fa-fw fa-thermometer-half', 886 | textField: 'sensorsOutput', 887 | renderer: function(value){ 888 | // sensors configuration 889 | const cpuTempHelper = Ext.create('PVE.mod.TempHelper', $HELPERCTORPARAMS); 890 | // display configuration 891 | const itemsPerRow = $CPU_ITEMS_PER_ROW; 892 | // --- 893 | let objValue; 894 | try { 895 | objValue = JSON.parse(value) || {}; 896 | } catch(e) { 897 | objValue = {}; 898 | } 899 | const cpuKeysI = Object.keys(objValue).filter(item => String(item).startsWith('coretemp-isa-')).sort(); 900 | const cpuKeysA = Object.keys(objValue).filter(item => String(item).startsWith('k10temp-pci-')).sort(); 901 | const bINTEL = cpuKeysI.length > 0 ? true : false; 902 | const INTELPackagePrefix = '$CPU_TEMP_TARGET' == 'Core' ? 'Core ' : 'Package id'; 903 | const INTELPackageCaption = '$CPU_TEMP_TARGET' == 'Core' ? 'Core' : 'Package'; 904 | let AMDPackagePrefix = 'Tccd'; 905 | let AMDPackageCaption = 'CCD'; 906 | 907 | if (cpuKeysA.length > 0) { 908 | let bTccd = false; 909 | let bTctl = false; 910 | let bTdie = false; 911 | let bCpuCoreTemp = false; 912 | cpuKeysA.forEach((cpuKey, cpuIndex) => { 913 | let items = objValue[cpuKey]; 914 | bTccd = Object.keys(items).findIndex(item => { return String(item).startsWith('Tccd'); }) >= 0; 915 | bTctl = Object.keys(items).findIndex(item => { return String(item).startsWith('Tctl'); }) >= 0; 916 | bTdie = Object.keys(items).findIndex(item => { return String(item).startsWith('Tdie'); }) >= 0; 917 | bCpuCoreTemp = Object.keys(items).findIndex(item => { return String(item) === 'CPU Core Temp'; }) >= 0; 918 | }); 919 | if (bTccd && '$CPU_TEMP_TARGET' == 'Core') { 920 | AMDPackagePrefix = 'Tccd'; 921 | AMDPackageCaption = 'ccd'; 922 | } else if (bCpuCoreTemp && '$CPU_TEMP_TARGET' == 'Package') { 923 | AMDPackagePrefix = 'CPU Core Temp'; 924 | AMDPackageCaption = 'CPU Core Temp'; 925 | } else if (bTdie) { 926 | AMDPackagePrefix = 'Tdie'; 927 | AMDPackageCaption = 'die'; 928 | } else if (bTctl) { 929 | AMDPackagePrefix = 'Tctl'; 930 | AMDPackageCaption = 'ctl'; 931 | } else { 932 | AMDPackagePrefix = 'temp'; 933 | AMDPackageCaption = 'Temp'; 934 | } 935 | } 936 | 937 | const cpuKeys = bINTEL ? cpuKeysI : cpuKeysA; 938 | const cpuItemPrefix = bINTEL ? INTELPackagePrefix : AMDPackagePrefix; 939 | const cpuTempCaption = bINTEL ? INTELPackageCaption : AMDPackageCaption; 940 | const formatTemp = bINTEL ? '0' : '0.0'; 941 | const cpuCount = cpuKeys.length; 942 | let temps = []; 943 | 944 | cpuKeys.forEach((cpuKey, cpuIndex) => { 945 | let cpuTemps = []; 946 | const items = objValue[cpuKey]; 947 | const itemKeys = Object.keys(items).filter(item => { 948 | if ('$CPU_TEMP_TARGET' == 'Core') { 949 | // In Core mode: only show individual cores/CCDs, exclude overall CPU temp 950 | return String(item).includes(cpuItemPrefix) || String(item).startsWith('Tccd'); 951 | } else { 952 | // In Package mode: show overall CPU temp and package-level readings 953 | return String(item).includes(cpuItemPrefix) || String(item) === 'CPU Core Temp'; 954 | } 955 | }); 956 | 957 | itemKeys.forEach((coreKey) => { 958 | try { 959 | let tempVal = NaN, tempMax = NaN, tempCrit = NaN; 960 | Object.keys(items[coreKey]).forEach((secondLevelKey) => { 961 | if (secondLevelKey.endsWith('_input')) { 962 | tempVal = cpuTempHelper.getTemp(parseFloat(items[coreKey][secondLevelKey])); 963 | } else if (secondLevelKey.endsWith('_max')) { 964 | tempMax = cpuTempHelper.getTemp(parseFloat(items[coreKey][secondLevelKey])); 965 | } else if (secondLevelKey.endsWith('_crit')) { 966 | tempCrit = cpuTempHelper.getTemp(parseFloat(items[coreKey][secondLevelKey])); 967 | } 968 | }); 969 | 970 | if (!isNaN(tempVal)) { 971 | let tempStyle = ''; 972 | if (!isNaN(tempMax) && tempVal >= tempMax) { 973 | tempStyle = 'color: #FFC300; font-weight: bold;'; 974 | } 975 | if (!isNaN(tempCrit) && tempVal >= tempCrit) { 976 | tempStyle = 'color: red; font-weight: bold;'; 977 | } 978 | 979 | let tempStr = ''; 980 | 981 | // Enhanced parsing for AMD temperatures 982 | if (coreKey.startsWith('Tccd')) { 983 | let tempIndex = coreKey.match(/Tccd(\d+)/); 984 | if (tempIndex !== null && tempIndex.length > 1) { 985 | tempIndex = tempIndex[1]; 986 | tempStr = `${cpuTempCaption} ${tempIndex}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()}`; 987 | } else { 988 | tempStr = `${cpuTempCaption}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()}`; 989 | } 990 | } 991 | // Handle CPU Core Temp (single overall temperature) 992 | else if (coreKey === 'CPU Core Temp') { 993 | tempStr = `${cpuTempCaption}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()}`; 994 | } 995 | // Enhanced parsing for Intel cores (P-Core, E-Core, regular Core) 996 | else { 997 | let tempIndex = coreKey.match(/(?:P\s+Core|E\s+Core|Core)\s*(\d+)/); 998 | if (tempIndex !== null && tempIndex.length > 1) { 999 | tempIndex = tempIndex[1]; 1000 | let coreType = coreKey.startsWith('P Core') ? 'P Core' : 1001 | coreKey.startsWith('E Core') ? 'E Core' : 1002 | cpuTempCaption; 1003 | tempStr = `${coreType} ${tempIndex}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()}`; 1004 | } else { 1005 | // fallback for CPUs which do not have a core index 1006 | let coreType = coreKey.startsWith('P Core') ? 'P Core' : 1007 | coreKey.startsWith('E Core') ? 'E Core' : 1008 | cpuTempCaption; 1009 | tempStr = `${coreType}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()}`; 1010 | } 1011 | } 1012 | 1013 | cpuTemps.push(tempStr); 1014 | } 1015 | } catch (e) { /*_*/ } 1016 | }); 1017 | 1018 | if(cpuTemps.length > 0) { 1019 | temps.push(cpuTemps); 1020 | } 1021 | }); 1022 | 1023 | let result = ''; 1024 | temps.forEach((cpuTemps, cpuIndex) => { 1025 | const strCoreTemps = cpuTemps.map((strTemp, index, arr) => { 1026 | return strTemp + (index + 1 < arr.length ? (itemsPerRow > 0 && (index + 1) % itemsPerRow === 0 ? '
' : ' | ') : ''); 1027 | }) 1028 | if(strCoreTemps.length > 0) { 1029 | result += (cpuCount > 1 ? `CPU ${cpuIndex+1}: ` : '') + strCoreTemps.join('') + (cpuIndex < cpuCount ? '
' : ''); 1030 | } 1031 | }); 1032 | 1033 | return '
' + (result.length > 0 ? result : 'N/A') + '
'; 1034 | } 1035 | }, 1036 | EOF 1037 | ) 1038 | #endregion cpu widget heredoc 1039 | if [[ $? -ne 0 ]]; then 1040 | echo "Error: Failed to generate cpu widget code" >&2 1041 | exit 1 1042 | fi 1043 | } 1044 | 1045 | # Function to generate nvme widget 1046 | generate_nvme_widget() { 1047 | #region nvme widget heredoc 1048 | # use subshell to allow variable expansion 1049 | ( 1050 | export HELPERCTORPARAMS 1051 | export NVME_ITEMS_PER_ROW 1052 | cat <<'EOF' | envsubst '$HELPERCTORPARAMS $NVME_ITEMS_PER_ROW' > "$1" 1053 | { 1054 | itemId: 'thermalNvme', 1055 | colspan: 2, 1056 | printBar: false, 1057 | title: gettext('NVMe Thermal State'), 1058 | iconCls: 'fa fa-fw fa-thermometer-half', 1059 | textField: 'sensorsOutput', 1060 | renderer: function(value) { 1061 | // sensors configuration 1062 | const addressPrefix = "nvme-pci-"; 1063 | const sensorName = "Composite"; 1064 | const tempHelper = Ext.create('PVE.mod.TempHelper', $HELPERCTORPARAMS); 1065 | // display configuration 1066 | const itemsPerRow = $NVME_ITEMS_PER_ROW; 1067 | // --- 1068 | let objValue; 1069 | try { 1070 | objValue = JSON.parse(value) || {}; 1071 | } catch(e) { 1072 | objValue = {}; 1073 | } 1074 | const nvmeKeys = Object.keys(objValue).filter(item => String(item).startsWith(addressPrefix)).sort(); 1075 | let temps = []; 1076 | nvmeKeys.forEach((nvmeKey, index) => { 1077 | try { 1078 | let tempVal = NaN, tempMax = NaN, tempCrit = NaN; 1079 | Object.keys(objValue[nvmeKey][sensorName]).forEach((secondLevelKey) => { 1080 | if (secondLevelKey.endsWith('_input')) { 1081 | tempVal = tempHelper.getTemp(parseFloat(objValue[nvmeKey][sensorName][secondLevelKey])); 1082 | } else if (secondLevelKey.endsWith('_max')) { 1083 | tempMax = tempHelper.getTemp(parseFloat(objValue[nvmeKey][sensorName][secondLevelKey])); 1084 | } else if (secondLevelKey.endsWith('_crit')) { 1085 | tempCrit = tempHelper.getTemp(parseFloat(objValue[nvmeKey][sensorName][secondLevelKey])); 1086 | } 1087 | }); 1088 | if (!isNaN(tempVal)) { 1089 | let tempStyle = ''; 1090 | if (!isNaN(tempMax) && tempVal >= tempMax) { 1091 | tempStyle = 'color: #FFC300; font-weight: bold;'; 1092 | } 1093 | if (!isNaN(tempCrit) && tempVal >= tempCrit) { 1094 | tempStyle = 'color: red; font-weight: bold;'; 1095 | } 1096 | const tempStr = `Drive ${index + 1}: ${Ext.util.Format.number(tempVal, '0.0')}${tempHelper.getUnit()}`; 1097 | temps.push(tempStr); 1098 | } 1099 | } catch(e) { /*_*/ } 1100 | }); 1101 | const result = temps.map((strTemp, index, arr) => { return strTemp + (index + 1 < arr.length ? ((index + 1) % itemsPerRow === 0 ? '
' : ' | ') : ''); }); 1102 | return '
' + (result.length > 0 ? result.join('') : 'N/A') + '
'; 1103 | } 1104 | }, 1105 | EOF 1106 | ) 1107 | #endregion nvme widget heredoc 1108 | if [[ $? -ne 0 ]]; then 1109 | echo "Error: Failed to generate nvme widget code" >&2 1110 | exit 1 1111 | fi 1112 | } 1113 | 1114 | # Function to generate Fan widget 1115 | generate_fan_widget() { 1116 | #region fan widget heredoc 1117 | # use subshell to allow variable expansion 1118 | ( 1119 | export DISPLAY_ZERO_SPEED_FANS 1120 | cat <<'EOF' | envsubst '$DISPLAY_ZERO_SPEED_FANS' > "$1" 1121 | { 1122 | xtype: 'box', 1123 | colspan: 2, 1124 | html: gettext('Cooling'), 1125 | }, 1126 | { 1127 | itemId: 'speedFan', 1128 | colspan: 2, 1129 | printBar: false, 1130 | title: gettext('Fan Speed(s)'), 1131 | iconCls: 'fa fa-fw fa-snowflake-o', 1132 | textField: 'sensorsOutput', 1133 | renderer: function(value) { 1134 | // --- 1135 | let objValue; 1136 | try { 1137 | objValue = JSON.parse(value) || {}; 1138 | } catch(e) { 1139 | objValue = {}; 1140 | } 1141 | 1142 | // Recursive function to find fan keys and values 1143 | function findFanKeys(obj, fanKeys, parentKey = null) { 1144 | Object.keys(obj).forEach(key => { 1145 | const value = obj[key]; 1146 | if (typeof value === 'object' && value !== null) { 1147 | // If the value is an object, recursively call the function 1148 | findFanKeys(value, fanKeys, key); 1149 | } else if (/^fan[0-9]+(_input)?$/.test(key)) { 1150 | if ($DISPLAY_ZERO_SPEED_FANS != true && value === 0) { 1151 | // Skip this fan if DISPLAY_ZERO_SPEED_FANS is false and value is 0 1152 | return; 1153 | } 1154 | // If the key matches the pattern, add the parent key and value to the fanKeys array 1155 | fanKeys.push({ key: parentKey, value: value }); 1156 | } 1157 | }); 1158 | } 1159 | 1160 | let speeds = []; 1161 | // Loop through the parent keys 1162 | Object.keys(objValue).forEach(parentKey => { 1163 | const parentObj = objValue[parentKey]; 1164 | // Array to store fan keys and values 1165 | const fanKeys = []; 1166 | // Call the recursive function to find fan keys and values 1167 | findFanKeys(parentObj, fanKeys); 1168 | // Sort the fan keys 1169 | fanKeys.sort(); 1170 | // Process each fan key and value 1171 | fanKeys.forEach(({ key: fanKey, value: fanSpeed }) => { 1172 | try { 1173 | const fan = fanKey.charAt(0).toUpperCase() + fanKey.slice(1); // Capitalize the first letter of fanKey 1174 | speeds.push(`${fan}: ${fanSpeed} RPM`); 1175 | } catch(e) { 1176 | console.error(`Error retrieving fan speed for ${fanKey} in ${parentKey}:`, e); // Debug: Log specific error 1177 | } 1178 | }); 1179 | }); 1180 | return '
' + (speeds.length > 0 ? speeds.join(' | ') : 'N/A') + '
'; 1181 | } 1182 | }, 1183 | EOF 1184 | ) 1185 | #endregion fan widget heredoc 1186 | if [[ $? -ne 0 ]]; then 1187 | echo "Error: Failed to generate fan widget code" >&2 1188 | exit 1 1189 | fi 1190 | } 1191 | 1192 | # Function to generate UPS widget 1193 | generate_hdd_widget() { 1194 | #region hdd widget heredoc 1195 | # use subshell to allow variable expansion 1196 | ( 1197 | export HELPERCTORPARAMS 1198 | export HDD_ITEMS_PER_ROW 1199 | cat <<'EOF' | envsubst '$HDD_ITEMS_PER_ROW $HELPERCTORPARAMS' > "$1" 1200 | { 1201 | itemId: 'thermalHdd', 1202 | colspan: 2, 1203 | printBar: false, 1204 | title: gettext('HDD/SSD Thermal State'), 1205 | iconCls: 'fa fa-fw fa-thermometer-half', 1206 | textField: 'sensorsOutput', 1207 | renderer: function(value) { 1208 | // sensors configuration 1209 | const addressPrefix = "drivetemp-scsi-"; 1210 | const sensorName = "temp1"; 1211 | const tempHelper = Ext.create('PVE.mod.TempHelper', $HELPERCTORPARAMS); 1212 | // display configuration 1213 | const itemsPerRow = $HDD_ITEMS_PER_ROW; 1214 | // --- 1215 | let objValue; 1216 | try { 1217 | objValue = JSON.parse(value) || {}; 1218 | } catch(e) { 1219 | objValue = {}; 1220 | } 1221 | const drvKeys = Object.keys(objValue).filter(item => String(item).startsWith(addressPrefix)).sort(); 1222 | let temps = []; 1223 | drvKeys.forEach((drvKey, index) => { 1224 | try { 1225 | let tempVal = NaN, tempMax = NaN, tempCrit = NaN; 1226 | Object.keys(objValue[drvKey][sensorName]).forEach((secondLevelKey) => { 1227 | if (secondLevelKey.endsWith('_input')) { 1228 | tempVal = tempHelper.getTemp(parseFloat(objValue[drvKey][sensorName][secondLevelKey])); 1229 | } else if (secondLevelKey.endsWith('_max')) { 1230 | tempMax = tempHelper.getTemp(parseFloat(objValue[drvKey][sensorName][secondLevelKey])); 1231 | } else if (secondLevelKey.endsWith('_crit')) { 1232 | tempCrit = tempHelper.getTemp(parseFloat(objValue[drvKey][sensorName][secondLevelKey])); 1233 | } 1234 | }); 1235 | if (!isNaN(tempVal)) { 1236 | let tempStyle = ''; 1237 | if (!isNaN(tempMax) && tempVal >= tempMax) { 1238 | tempStyle = 'color: #FFC300; font-weight: bold;'; 1239 | } 1240 | if (!isNaN(tempCrit) && tempVal >= tempCrit) { 1241 | tempStyle = 'color: red; font-weight: bold;'; 1242 | } 1243 | const tempStr = `Drive ${index + 1}: ${Ext.util.Format.number(tempVal, '0.0')}${tempHelper.getUnit()}`; 1244 | temps.push(tempStr); 1245 | } 1246 | } catch(e) { /*_*/ } 1247 | }); 1248 | const result = temps.map((strTemp, index, arr) => { return strTemp + (index + 1 < arr.length ? ((index + 1) % itemsPerRow === 0 ? '
' : ' | ') : ''); }); 1249 | return '
' + (result.length > 0 ? result.join('') : 'N/A') + '
'; 1250 | } 1251 | }, 1252 | EOF 1253 | ) 1254 | #endregion hdd widget heredoc 1255 | if [[ $? -ne 0 ]]; then 1256 | echo "Error: Failed to generate hhd widget code" >&2 1257 | exit 1 1258 | fi 1259 | } 1260 | 1261 | # Function to generate RAM widget 1262 | generate_ram_widget() { 1263 | #region ram widget heredoc 1264 | # use subshell to allow variable expansion 1265 | ( 1266 | export HELPERCTORPARAMS 1267 | cat <<'EOF' | envsubst '$HELPERCTORPARAMS' > "$1" 1268 | { 1269 | xtype: 'box', 1270 | colspan: 2, 1271 | html: gettext('RAM'), 1272 | }, 1273 | { 1274 | itemId: 'thermalRam', 1275 | colspan: 2, 1276 | printBar: false, 1277 | title: gettext('Thermal State'), 1278 | iconCls: 'fa fa-fw fa-thermometer-half', 1279 | textField: 'sensorsOutput', 1280 | renderer: function(value) { 1281 | const cpuTempHelper = Ext.create('PVE.mod.TempHelper', $HELPERCTORPARAMS); 1282 | 1283 | let objValue; 1284 | try { 1285 | objValue = JSON.parse(value) || {}; 1286 | } catch(e) { 1287 | objValue = {}; 1288 | } 1289 | 1290 | // Recursive function to find ram keys and values 1291 | function findRamKeys(obj, ramKeys, parentKey = null) { 1292 | Object.keys(obj).forEach(key => { 1293 | const value = obj[key]; 1294 | if (typeof value === 'object' && value !== null) { 1295 | // If the value is an object, recursively call the function 1296 | findRamKeys(value, ramKeys, key); 1297 | } else if (/^temp\d+_input$/.test(key) && parentKey && parentKey.startsWith("SODIMM")) { 1298 | if (value !== 0) { 1299 | ramKeys.push({ key: parentKey, value: value}); 1300 | } 1301 | } 1302 | }); 1303 | } 1304 | 1305 | let ramTemps = []; 1306 | // Loop through the parent keys 1307 | Object.keys(objValue).forEach(parentKey => { 1308 | const parentObj = objValue[parentKey]; 1309 | // Array to store ram keys and values 1310 | const ramKeys = []; 1311 | // Call the recursive function to find ram keys and values 1312 | findRamKeys(parentObj, ramKeys); 1313 | // Sort the ramKeys keys 1314 | ramKeys.sort(); 1315 | // Process each ram key and value 1316 | ramKeys.forEach(({ key: ramKey, value: ramTemp }) => { 1317 | try { 1318 | ramTemps.push(`${ramKey}: ${ramTemp}${cpuTempHelper.getUnit()}`); 1319 | } catch(e) { 1320 | console.error(`Error retrieving Ram Temp for ${ramTemps} in ${parentKey}:`, e); // Debug: Log specific error 1321 | } 1322 | }); 1323 | }); 1324 | return '
' + (ramTemps.length > 0 ? ramTemps.join(' | ') : 'N/A') + '
'; 1325 | } 1326 | }, 1327 | EOF 1328 | ) 1329 | #endregion ram widget heredoc 1330 | if [[ $? -ne 0 ]]; then 1331 | echo "Error: Failed to generate ram widget code" >&2 1332 | exit 1 1333 | fi 1334 | } 1335 | 1336 | # Function to generate UPS widget 1337 | generate_ups_widget() { 1338 | #region UPS widget heredoc 1339 | cat > "$1" <<'EOF' 1340 | { 1341 | xtype: 'box', 1342 | colspan: 2, 1343 | html: gettext('UPS'), 1344 | }, 1345 | { 1346 | itemId: 'upsc', 1347 | colspan: 2, 1348 | printBar: false, 1349 | title: gettext('Device'), 1350 | iconCls: 'fa fa-fw fa-battery-three-quarters', 1351 | textField: 'upsc', 1352 | renderer: function(value) { 1353 | let objValue = {}; 1354 | try { 1355 | // Parse the UPS data 1356 | if (typeof value === 'string') { 1357 | const lines = value.split('\n'); 1358 | lines.forEach(line => { 1359 | const colonIndex = line.indexOf(':'); 1360 | if (colonIndex > 0) { 1361 | const key = line.substring(0, colonIndex).trim(); 1362 | const val = line.substring(colonIndex + 1).trim(); 1363 | objValue[key] = val; 1364 | } 1365 | }); 1366 | } else if (typeof value === 'object') { 1367 | objValue = value || {}; 1368 | } 1369 | } catch(e) { 1370 | objValue = {}; 1371 | } 1372 | 1373 | // If objValue is null or empty, return N/A 1374 | if (!objValue || Object.keys(objValue).length === 0) { 1375 | return '
N/A
'; 1376 | } 1377 | 1378 | // Helper function to get status color 1379 | // Returns a CSS color string for non-default states, or null for default (no inline color) 1380 | function getStatusColor(status) { 1381 | if (!status) return '#999'; 1382 | const statusUpper = status.toUpperCase(); 1383 | if (statusUpper.includes('OL')) return null; // default (no explicit color) 1384 | if (statusUpper.includes('OB')) return '#d9534f'; // Red for on battery 1385 | if (statusUpper.includes('LB')) return '#d9534f'; // Red for low battery 1386 | return '#f0ad4e'; // Orange for other states 1387 | } 1388 | 1389 | // Helper function to get load/charge color 1390 | // Returns null for default/good values so no inline style is emitted 1391 | function getPercentageColor(value, isLoad = false) { 1392 | if (!value || isNaN(value)) return '#999'; 1393 | const num = parseFloat(value); 1394 | if (isLoad) { 1395 | if (num >= 80) return '#d9534f'; // Red for high load 1396 | if (num >= 60) return '#f0ad4e'; // Orange for medium load 1397 | return null; // default (no explicit color) 1398 | } else { 1399 | // For battery charge 1400 | if (num <= 20) return '#d9534f'; // Red for low charge 1401 | if (num <= 50) return '#f0ad4e'; // Orange for medium charge 1402 | return null; // default (no explicit color) 1403 | } 1404 | } 1405 | 1406 | // Helper function to format runtime 1407 | function formatRuntime(seconds) { 1408 | if (!seconds || isNaN(seconds)) return 'N/A'; 1409 | const mins = Math.floor(seconds / 60); 1410 | const secs = seconds % 60; 1411 | return `${mins}m ${secs}s`; 1412 | } 1413 | 1414 | // Extract key UPS information 1415 | const batteryCharge = objValue['battery.charge']; 1416 | const batteryRuntime = objValue['battery.runtime']; 1417 | const inputVoltage = objValue['input.voltage']; 1418 | const upsLoad = objValue['ups.load']; 1419 | const upsStatus = objValue['ups.status']; 1420 | const upsModel = objValue['ups.model'] || objValue['device.model']; 1421 | const testResult = objValue['ups.test.result']; 1422 | const batteryChargeLow = objValue['battery.charge.low']; 1423 | const batteryRuntimeLow = objValue['battery.runtime.low']; 1424 | const upsRealPowerNominal = objValue['ups.realpower.nominal']; 1425 | const batteryMfrDate = objValue['battery.mfr.date']; 1426 | 1427 | // Build the status display 1428 | let displayItems = []; 1429 | 1430 | // First line: Model info (no explicit color for default) 1431 | let modelLine = ''; 1432 | if (upsModel) { 1433 | modelLine = `${upsModel}`; 1434 | } else { 1435 | modelLine = `N/A`; 1436 | } 1437 | displayItems.push(modelLine); 1438 | 1439 | // Main status line with all metrics 1440 | let statusLine = ''; 1441 | 1442 | // Status 1443 | if (upsStatus) { 1444 | const statusUpper = upsStatus.toUpperCase(); 1445 | let statusText = 'Unknown'; 1446 | let statusColor = '#f0ad4e'; 1447 | 1448 | if (statusUpper.includes('OL')) { 1449 | statusText = 'Online'; 1450 | statusColor = null; // default (no explicit color) 1451 | } else if (statusUpper.includes('OB')) { 1452 | statusText = 'On Battery'; 1453 | statusColor = '#d9534f'; // Red for on battery 1454 | } else if (statusUpper.includes('LB')) { 1455 | statusText = 'Low Battery'; 1456 | statusColor = '#d9534f'; // Red for low battery 1457 | } else { 1458 | statusText = upsStatus; 1459 | statusColor = '#f0ad4e'; // Orange for unknown status 1460 | } 1461 | 1462 | let statusStyle = statusColor ? ('color: ' + statusColor + ';') : ''; 1463 | statusLine += 'Status: ' + statusText + ''; 1464 | } else { 1465 | statusLine += 'Status: N/A'; 1466 | } 1467 | 1468 | // Battery charge 1469 | if (statusLine) statusLine += ' | '; 1470 | if (batteryCharge) { 1471 | const chargeColor = getPercentageColor(batteryCharge, false); 1472 | let chargeStyle = chargeColor ? ('color: ' + chargeColor + ';') : ''; 1473 | statusLine += 'Battery: ' + batteryCharge + '%'; 1474 | } else { 1475 | statusLine += 'Battery: N/A'; 1476 | } 1477 | 1478 | // Load percentage 1479 | if (statusLine) statusLine += ' | '; 1480 | if (upsLoad) { 1481 | const loadColor = getPercentageColor(upsLoad, true); 1482 | let loadStyle = loadColor ? ('color: ' + loadColor + ';') : ''; 1483 | statusLine += 'Load: ' + upsLoad + '%'; 1484 | } else { 1485 | statusLine += 'Load: N/A'; 1486 | } 1487 | 1488 | // Runtime 1489 | if (statusLine) statusLine += ' | '; 1490 | if (batteryRuntime) { 1491 | const runtime = parseInt(batteryRuntime); 1492 | const runtimeLowThreshold = batteryRuntimeLow ? parseInt(batteryRuntimeLow) : 600; 1493 | let runtimeColor = null; 1494 | if (runtime <= runtimeLowThreshold / 2) runtimeColor = '#d9534f'; // Red if less than half of low threshold 1495 | else if (runtime <= runtimeLowThreshold) runtimeColor = '#f0ad4e'; // Orange if at low threshold 1496 | let runtimeStyle = runtimeColor ? ('color: ' + runtimeColor + ';') : ''; 1497 | statusLine += 'Runtime: ' + formatRuntime(runtime) + ''; 1498 | } else { 1499 | statusLine += 'Runtime: N/A'; 1500 | } 1501 | 1502 | // Input voltage 1503 | if (statusLine) statusLine += ' | '; 1504 | if (inputVoltage) { 1505 | statusLine += 'Input: ' + parseFloat(inputVoltage).toFixed(0) + 'V'; 1506 | } else { 1507 | statusLine += 'Input: N/A'; 1508 | } 1509 | 1510 | // Calculate actual watt usage 1511 | if (statusLine) statusLine += ' | '; 1512 | let actualWattage = null; 1513 | if (upsLoad && upsRealPowerNominal) { 1514 | const load = parseFloat(upsLoad); 1515 | const nominal = parseFloat(upsRealPowerNominal); 1516 | if (!isNaN(load) && !isNaN(nominal)) { 1517 | actualWattage = Math.round((load / 100) * nominal); 1518 | } 1519 | } 1520 | 1521 | // Real power (calculated watt usage) 1522 | if (actualWattage !== null) { 1523 | statusLine += 'Output: ' + actualWattage + 'W'; 1524 | } else { 1525 | statusLine += 'Output: N/A'; 1526 | } 1527 | 1528 | displayItems.push(statusLine); 1529 | 1530 | // Combined battery and test line 1531 | let batteryTestLine = ''; 1532 | if (batteryMfrDate) { 1533 | batteryTestLine += 'Battery MFD: ' + batteryMfrDate + ''; 1534 | } else { 1535 | batteryTestLine += 'Battery MFD: N/A'; 1536 | } 1537 | 1538 | if (testResult && !testResult.toLowerCase().includes('no test')) { 1539 | const testColor = testResult.toLowerCase().includes('passed') ? null : '#d9534f'; 1540 | let testStyle = testColor ? ('color: ' + testColor + ';') : ''; 1541 | batteryTestLine += ' | Test: ' + testResult + ''; 1542 | } else { 1543 | batteryTestLine += ' | Test: N/A'; 1544 | } 1545 | 1546 | displayItems.push(batteryTestLine); 1547 | 1548 | // Format the final output 1549 | return '
' + displayItems.join('
') + '
'; 1550 | } 1551 | }, 1552 | EOF 1553 | #endregion UPS widget heredoc 1554 | if [[ $? -ne 0 ]]; then 1555 | echo "Error: Failed to generate UPS widget code" >&2 1556 | exit 1 1557 | fi 1558 | } 1559 | 1560 | #endregion widget generation functions 1561 | 1562 | # Function to uninstall the modification 1563 | function uninstall_mod { 1564 | msgb "=== Uninstalling Mod ===" 1565 | 1566 | check_root_privileges 1567 | 1568 | if [[ -z $(grep -e "$res->{sensorsOutput}" "$NODES_PM_FILE") ]] && [[ -z $(grep -e "$res->{systemInfo}" "$NODES_PM_FILE") ]]; then 1569 | err "Mod is not installed." 1570 | fi 1571 | 1572 | set_backup_directory 1573 | info "Restoring modified files..." 1574 | 1575 | # Find the latest Nodes.pm file using the find command 1576 | local latest_nodes_pm=$(find "$BACKUP_DIR" -name "Nodes.pm.*" -type f -printf '%T+ %p\n' 2>/dev/null | sort -r | head -n 1 | awk '{print $2}') 1577 | 1578 | if [ -n "$latest_nodes_pm" ]; then 1579 | # Remove the latest Nodes.pm file 1580 | msgb "Restoring latest Nodes.pm from backup: $latest_nodes_pm to \"$NODES_PM_FILE\"." 1581 | cp "$latest_nodes_pm" "$NODES_PM_FILE" 1582 | info "Restored Nodes.pm successfully." 1583 | else 1584 | warn "No Nodes.pm backup files found." 1585 | fi 1586 | 1587 | # Find the latest pvemanagerlib.js file using the find command 1588 | local latest_pvemanagerlibjs=$(find "$BACKUP_DIR" -name "pvemanagerlib.js.*" -type f -printf '%T+ %p\n' 2>/dev/null | sort -r | head -n 1 | awk '{print $2}') 1589 | 1590 | if [ -n "$latest_pvemanagerlibjs" ]; then 1591 | # Remove the latest pvemanagerlib.js file 1592 | msgb "Restoring latest pvemanagerlib.js from backup: $latest_pvemanagerlibjs to \"$PVE_MANAGER_LIB_JS_FILE\"." 1593 | cp "$latest_pvemanagerlibjs" "$PVE_MANAGER_LIB_JS_FILE" 1594 | info "Restored pvemanagerlib.js successfully." 1595 | else 1596 | warn "No pvemanagerlib.js backup files found." 1597 | fi 1598 | 1599 | if [ -n "$latest_nodes_pm" ] || [ -n "$latest_pvemanagerlibjs" ]; then 1600 | # At least one of the variables is not empty, restart the proxy 1601 | restart_proxy 1602 | fi 1603 | 1604 | ask "Clear the browser cache to ensure all changes are visualized. (any key to continue)" 1605 | } 1606 | 1607 | # Function to check if the modification is installed 1608 | check_mod_installation() { 1609 | if [[ -n $(grep -F '$res->{sensorsOutput}' "$NODES_PM_FILE") ]] && \ 1610 | [[ -n $(grep -F '$res->{systemInfo}' "$NODES_PM_FILE") ]] && \ 1611 | [[ -n $(grep -E "itemId: 'thermal[[:alnum:]]*'" "$PVE_MANAGER_LIB_JS_FILE") ]]; then 1612 | err "Mod is already installed. Uninstall existing before installing." 1613 | fi 1614 | } 1615 | 1616 | function restart_proxy { 1617 | # Restart pveproxy 1618 | info "Restarting PVE proxy..." 1619 | systemctl restart pveproxy 1620 | } 1621 | 1622 | function save_sensors_data { 1623 | msgb "=== Saving Sensors Data ===" 1624 | 1625 | # Check if JSON_EXPORT_DIRECTORY exists and is writable 1626 | if [[ ! -d "$JSON_EXPORT_DIRECTORY" || ! -w "$JSON_EXPORT_DIRECTORY" ]]; then 1627 | err "Directory $JSON_EXPORT_DIRECTORY does not exist or is not writable. No file could be saved." 1628 | return 1629 | fi 1630 | 1631 | 1632 | # Check if command exists 1633 | if (command -v sensors &>/dev/null); then 1634 | # Save sensors output 1635 | local debug_save_filename="sensorsdata.json" 1636 | local filepath="${JSON_EXPORT_DIRECTORY}/${debug_save_filename}" 1637 | msgb "Sensors data will be saved in $filepath" 1638 | 1639 | # Prompt user for confirmation 1640 | local choiceContinue=$(ask "Do you wish to continue? (Y/n)") 1641 | case "$choiceContinue" in 1642 | [yY]|"") 1643 | echo "lm-sensors raw output:" >"$filepath" 1644 | sensorsOutput=$(sensors -j 2>/dev/null) 1645 | echo "$sensorsOutput" >>"$filepath" 1646 | echo -e "\n\nSanitised lm-sensors output:" >>"$filepath" 1647 | # Apply lm-sensors sanitization 1648 | sanitisedSensorsOutput=$(sanitize_sensors_output "$sensorsOutput") 1649 | echo "$sanitisedSensorsOutput" >>"$filepath" 1650 | info "Sensors data saved in $filepath." 1651 | ;; 1652 | *) 1653 | warn "Operation cancelled by user." 1654 | ;; 1655 | esac 1656 | else 1657 | err "Sensors is not installed. No file could be saved." 1658 | fi 1659 | echo 1660 | } 1661 | 1662 | function set_backup_directory { 1663 | # Check if the BACKUP_DIR variable is set, if not, use the default backup 1664 | if [[ -z "$BACKUP_DIR" ]]; then 1665 | # If not set, use the default backup directory, which is based on the home directory and PVE-MODS 1666 | BACKUP_DIR="$HOME/PVE-MODS" 1667 | info "Using default backup directory: $BACKUP_DIR" 1668 | else 1669 | # If set, ensure it is a valid directory 1670 | if [[ ! -d "$BACKUP_DIR" ]]; then 1671 | err "The specified backup directory does not exist: $BACKUP_DIR" 1672 | fi 1673 | info "Using custom backup directory: $BACKUP_DIR" 1674 | fi 1675 | } 1676 | 1677 | function create_backup_directory { 1678 | set_backup_directory 1679 | 1680 | # Create the backup directory if it does not exist 1681 | if [[ ! -d "$BACKUP_DIR" ]]; then 1682 | mkdir -p "$BACKUP_DIR" 2>/dev/null || { 1683 | err "Failed to create backup directory: $BACKUP_DIR. Please check permissions." 1684 | } 1685 | info "Created backup directory: $BACKUP_DIR" 1686 | else 1687 | info "Backup directory already exists: $BACKUP_DIR" 1688 | fi 1689 | } 1690 | 1691 | function create_file_backup() { 1692 | local source_file="$1" 1693 | local timestamp="$2" 1694 | local filename 1695 | 1696 | filename=$(basename "$source_file") 1697 | local backup_file="$BACKUP_DIR/${filename}.$timestamp" 1698 | 1699 | [[ -f "$source_file" ]] || err "Source file does not exist: $source_file" 1700 | [[ -r "$source_file" ]] || err "Cannot read source file: $source_file" 1701 | 1702 | cp "$source_file" "$backup_file" || err "Failed to create backup: $backup_file" 1703 | 1704 | # Verify backup integrity 1705 | if ! cmp -s "$source_file" "$backup_file"; then 1706 | err "Backup verification failed for: $backup_file" 1707 | fi 1708 | 1709 | info "Created backup: $backup_file" 1710 | } 1711 | 1712 | function perform_backup { 1713 | local timestamp 1714 | timestamp=$(date +%Y%m%d_%H%M%S) 1715 | 1716 | msgb "\n=== Creating backups of modified files ===" 1717 | 1718 | create_backup_directory 1719 | create_file_backup "$NODES_PM_FILE" "$timestamp" 1720 | create_file_backup "$PVE_MANAGER_LIB_JS_FILE" "$timestamp" 1721 | } 1722 | 1723 | # Process the arguments using a while loop and a case statement 1724 | executed=0 1725 | while [[ $# -gt 0 ]]; do 1726 | case "$1" in 1727 | install) 1728 | executed=$(($executed + 1)) 1729 | msgb "\nInstalling the Proxmox VE sensors display mod..." 1730 | install_packages 1731 | install_mod 1732 | ;; 1733 | uninstall) 1734 | executed=$(($executed + 1)) 1735 | msgb "\nUninstalling the Proxmox VE sensors display mod..." 1736 | uninstall_mod 1737 | ;; 1738 | save-sensors-data) 1739 | executed=$(($executed + 1)) 1740 | msgb "\nSaving current sensor readings in a file for debugging..." 1741 | save_sensors_data 1742 | ;; 1743 | esac 1744 | shift 1745 | done 1746 | 1747 | # If no arguments were provided or all arguments have been processed, print the usage message 1748 | if [[ $executed -eq 0 ]]; then 1749 | usage 1750 | fi 1751 | --------------------------------------------------------------------------------