├── pve-mod-nvidia.png ├── 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-nvidia.sh └── pve-mod-gui-sensors.sh /pve-mod-nvidia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j4ys0n/PVE-mods/HEAD/pve-mod-nvidia.png -------------------------------------------------------------------------------- /pve-mod-sensors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j4ys0n/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 | ## NVIDIA GPU readings view 53 | ![Proxmox NVIDIA GPU mod](pve-mod-nvidia.png) 54 | 55 | This bash script installs a modification to the Proxmox Virtual Environment (PVE) web user interface (UI) to display NVIDIA GPU information in the node status view. 56 | 57 | The following readings are displayed (per GPU): 58 | - GPU name and index 59 | - Temperature (Celsius/Fahrenheit) 60 | - GPU utilization 61 | - Memory utilization, used/total 62 | - Power draw / power limit 63 | - Fan speed (if supported) 64 | 65 | ### How it works 66 | The modification involves the following steps: 67 | 1. Backup original files in a backup directory (default: `~/PVE-MODS`, configurable via `BACKUP_DIR` in the script) 68 | - `/usr/share/pve-manager/js/pvemanagerlib.js` 69 | - `/usr/share/perl5/PVE/API2/Nodes.pm` 70 | 2. Patch `Nodes.pm` to add an API field (`nvidiaGpuOutput`) populated by `nvidia-smi`. 71 | 3. Modify `pvemanagerlib.js` to insert a new StatusView widget (“NVIDIA GPU Status”) before the CPU widget. 72 | 4. Restart the `pveproxy` service to apply changes. 73 | 74 | The script provides two options: 75 | | **Option** | **Description** | 76 | |-------------|------------------| 77 | | `install` | Apply the modification. | 78 | | `uninstall` | Restore original files from backups (see note below). | 79 | 80 | Notes: 81 | - Requires NVIDIA drivers installed on the Proxmox host (`nvidia-smi` must be available). 82 | - If you have other PVE UI mods installed (e.g. the sensors UI mod), uninstalling via backup restore may revert other changes depending on backup order. The script will warn if it detects other mods. 83 | - Proxmox upgrades may overwrite modified files; reinstallation of this mod could be required. 84 | 85 | ### Install 86 | Instructions be performed as 'root', as normal users do not have access to the files. 87 | 88 | ``` 89 | # Ensure NVIDIA drivers are installed and nvidia-smi works. 90 | # (Example check) 91 | nvidia-smi 92 | 93 | wget https://raw.githubusercontent.com/Meliox/PVE-mods/refs/heads/main/pve-mod-gui-nvidia.sh 94 | bash pve-mod-gui-nvidia.sh install 95 | # Then clear the browser cache to ensure all changes are visualized. 96 | ``` 97 | 98 | ## Nag screen deactivation 99 | (Tested compatibility: 7.x - 8.3.5) 100 | This bash script installs a modification to the Proxmox Virtual Environment (PVE) web user interface (UI) which deactivates the subscription nag screen. 101 | 102 | The modification includes two main steps: 103 | 1. Create backups of the original files in the `backup` directory relative to the script location. 104 | 2. Modify code. 105 | 106 | The script provides three options: 107 | | **Option** | **Description** | 108 | |-------------------------|-----------------------------------------------------------------------------| 109 | | `install` | Installs the modification by applying the necessary changes. | 110 | | `uninstall` | Removes the modification by restoring the original files from backups. | 111 | 112 | ### Install 113 | Instructions be performed as 'root', as normal users do not have access to the files. 114 | ``` 115 | wget https://raw.githubusercontent.com/Meliox/PVE-mods/refs/heads/main/pve-mod-nag-screen.sh 116 | bash pve-mod-nag-screen.sh install 117 | ``` 118 | 119 | ## Script to update all containers 120 | (Tested compatibility: 7.x - 8.3.5) 121 | 122 | This script updates all running Proxmox containers, skipping specified excluded containers, and generates a separate log file for each container. 123 | The script first updates the Proxmox host system, then iterates through each container, updates the container, and reboots it if necessary. 124 | Each container's log file is stored in $log_path and the main script log file is named container-upgrade-main.log. 125 | 126 | ### Install 127 | ``` 128 | wget https://raw.githubusercontent.com/Meliox/PVE-mods/refs/heads/main/updateallcontainers.sh 129 | ``` 130 | Or use git clone. 131 | Can be added to cron for e.g. monthly update: ```0 6 1 * * /root/scripts/updateallcontainers.sh``` 132 | -------------------------------------------------------------------------------- /pve-mod-gui-nvidia.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # This bash script installs a modification to the Proxmox Virtual Environment (PVE) 4 | # web user interface (UI) to display NVIDIA GPU information. 5 | # 6 | # Author: Based on pve-mod-gui-sensors.sh by Meliox 7 | # License: MIT 8 | # 9 | 10 | ################### Configuration ############# 11 | 12 | # Temperature thresholds (Celsius) 13 | TEMP_WARNING=70 14 | TEMP_CRITICAL=85 15 | 16 | # Overwrite default backup location (leave empty for default ~/PVE-MODS) 17 | BACKUP_DIR="" 18 | 19 | ##################### DO NOT EDIT BELOW ####################### 20 | 21 | # This script's working directory 22 | SCRIPT_CWD="$(dirname "$(readlink -f "$0")")" 23 | 24 | # File paths 25 | PVE_MANAGER_LIB_JS_FILE="/usr/share/pve-manager/js/pvemanagerlib.js" 26 | NODES_PM_FILE="/usr/share/perl5/PVE/API2/Nodes.pm" 27 | 28 | #region message tools 29 | # Section header (bold) 30 | function msgb() { 31 | local message="$1" 32 | echo -e "\e[1m${message}\e[0m" 33 | } 34 | 35 | # Info (green) 36 | function info() { 37 | local message="$1" 38 | echo -e "\e[0;32m[info] ${message}\e[0m" 39 | } 40 | 41 | # Warning (yellow) 42 | function warn() { 43 | local message="$1" 44 | echo -e "\e[0;33m[warning] ${message}\e[0m" 45 | } 46 | 47 | # Error (red) 48 | function err() { 49 | local message="$1" 50 | echo -e "\e[0;31m[error] ${message}\e[0m" 51 | exit 1 52 | } 53 | 54 | # Prompts (cyan) 55 | function ask() { 56 | local prompt="$1" 57 | local response 58 | read -p $'\n\e[1;36m'"${prompt}:"$'\e[0m ' response 59 | echo "$response" 60 | } 61 | #endregion message tools 62 | 63 | # Function to display usage information 64 | function usage { 65 | msgb "\nUsage:\n$0 [install | uninstall]\n" 66 | msgb "Options:" 67 | echo " install Install the NVIDIA GPU monitoring modification" 68 | echo " uninstall Remove the modification and restore original files" 69 | echo "" 70 | exit 1 71 | } 72 | 73 | # System checks 74 | function check_root_privileges() { 75 | if [[ $EUID -ne 0 ]]; then 76 | err "This script must be run as root. Please run it with 'sudo $0'." 77 | fi 78 | info "Root privileges verified." 79 | } 80 | 81 | # Check if nvidia-smi is available 82 | function check_nvidia_smi() { 83 | if ! command -v nvidia-smi &>/dev/null; then 84 | err "nvidia-smi is not installed or not in PATH. Please install NVIDIA drivers first." 85 | fi 86 | info "nvidia-smi found." 87 | } 88 | 89 | # Detect NVIDIA GPUs 90 | function detect_gpus() { 91 | local gpu_count 92 | gpu_count=$(nvidia-smi --query-gpu=count --format=csv,noheader,nounits 2>/dev/null | head -1) 93 | 94 | if [[ -z "$gpu_count" ]] || [[ "$gpu_count" -eq 0 ]]; then 95 | err "No NVIDIA GPUs detected by nvidia-smi." 96 | fi 97 | 98 | echo "$gpu_count" 99 | } 100 | 101 | # Configure installation options 102 | function configure() { 103 | msgb "\n=== Detecting NVIDIA GPUs ===" 104 | 105 | check_nvidia_smi 106 | 107 | local gpu_count 108 | gpu_count=$(detect_gpus) 109 | 110 | info "Detected $gpu_count NVIDIA GPU(s):" 111 | 112 | # Display detected GPUs 113 | nvidia-smi --query-gpu=index,name --format=csv,noheader 2>/dev/null | while read -r line; do 114 | echo " GPU $line" 115 | done 116 | 117 | # Temperature unit selection 118 | msgb "\n=== Display Settings ===" 119 | local unit 120 | unit=$(ask "Display temperatures in Celsius [C] or Fahrenheit [f]? (C/f)") 121 | case "$unit" in 122 | [fF]) 123 | TEMP_UNIT="F" 124 | info "Using Fahrenheit." 125 | ;; 126 | *) 127 | TEMP_UNIT="C" 128 | info "Using Celsius." 129 | ;; 130 | esac 131 | } 132 | 133 | # Function to check if the modification is already installed 134 | function check_mod_installation() { 135 | if grep -q 'nvidiaGpuOutput' "$NODES_PM_FILE" 2>/dev/null; then 136 | err "NVIDIA GPU mod is already installed. Please uninstall first before reinstalling." 137 | fi 138 | } 139 | 140 | # Set backup directory 141 | function set_backup_directory() { 142 | if [[ -z "$BACKUP_DIR" ]]; then 143 | BACKUP_DIR="$HOME/PVE-MODS" 144 | info "Using default backup directory: $BACKUP_DIR" 145 | else 146 | if [[ ! -d "$BACKUP_DIR" ]]; then 147 | err "The specified backup directory does not exist: $BACKUP_DIR" 148 | fi 149 | info "Using custom backup directory: $BACKUP_DIR" 150 | fi 151 | } 152 | 153 | # Create backup directory 154 | function create_backup_directory() { 155 | set_backup_directory 156 | 157 | if [[ ! -d "$BACKUP_DIR" ]]; then 158 | mkdir -p "$BACKUP_DIR" 2>/dev/null || { 159 | err "Failed to create backup directory: $BACKUP_DIR. Please check permissions." 160 | } 161 | info "Created backup directory: $BACKUP_DIR" 162 | else 163 | info "Backup directory already exists: $BACKUP_DIR" 164 | fi 165 | } 166 | 167 | # Create file backup 168 | function create_file_backup() { 169 | local source_file="$1" 170 | local timestamp="$2" 171 | local filename 172 | 173 | filename=$(basename "$source_file") 174 | local backup_file="$BACKUP_DIR/nvidia-gpu.${filename}.$timestamp" 175 | 176 | [[ -f "$source_file" ]] || err "Source file does not exist: $source_file" 177 | [[ -r "$source_file" ]] || err "Cannot read source file: $source_file" 178 | 179 | cp "$source_file" "$backup_file" || err "Failed to create backup: $backup_file" 180 | 181 | # Verify backup integrity 182 | if ! cmp -s "$source_file" "$backup_file"; then 183 | err "Backup verification failed for: $backup_file" 184 | fi 185 | 186 | info "Created backup: $backup_file" 187 | } 188 | 189 | # Perform backup of files 190 | function perform_backup() { 191 | local timestamp 192 | timestamp=$(date +%Y%m%d_%H%M%S) 193 | 194 | msgb "\n=== Creating backups of modified files ===" 195 | 196 | create_backup_directory 197 | create_file_backup "$NODES_PM_FILE" "$timestamp" 198 | create_file_backup "$PVE_MANAGER_LIB_JS_FILE" "$timestamp" 199 | } 200 | 201 | # Restart pveproxy service 202 | function restart_proxy() { 203 | info "Restarting PVE proxy..." 204 | systemctl restart pveproxy 205 | } 206 | 207 | # Insert NVIDIA GPU data collection into Nodes.pm 208 | function insert_node_info() { 209 | msgb "\n=== Inserting NVIDIA GPU data retrieval code ===" 210 | 211 | local nvidia_cmd='nvidia-smi --query-gpu=index,name,temperature.gpu,utilization.gpu,utilization.memory,memory.used,memory.total,power.draw,power.limit,fan.speed --format=csv,noheader,nounits 2>/dev/null' 212 | 213 | # Insert the nvidia-smi command into Nodes.pm 214 | # This adds a new field to the API response 215 | sed -i "/my \$dinfo = df('\/', 1);/i\\ 216 | # Collect NVIDIA GPU data\\ 217 | \$res->{nvidiaGpuOutput} = \`$nvidia_cmd\`;\\ 218 | " "$NODES_PM_FILE" 219 | 220 | if [[ $? -ne 0 ]]; then 221 | err "Failed to insert NVIDIA GPU retrieval code into Nodes.pm" 222 | fi 223 | 224 | info "NVIDIA GPU retriever added to \"$NODES_PM_FILE\"." 225 | } 226 | 227 | # Insert GPU widget before the 'cpus' item in pvemanagerlib.js 228 | function insert_gpu_widget() { 229 | msgb "\n=== Inserting GPU widget into UI ===" 230 | 231 | local temp_js_file="/tmp/nvidia_gpu_widget.js" 232 | 233 | # Generate the GPU widget JavaScript code with proper indentation 234 | # Indentation matches the pvemanagerlib.js style (tab-based) 235 | cat > "$temp_js_file" << 'WIDGET_EOF' 236 | { 237 | xtype: 'box', 238 | colspan: 2, 239 | html: gettext('GPU(s)'), 240 | }, 241 | { 242 | itemId: 'nvidiaGpu', 243 | colspan: 2, 244 | printBar: false, 245 | title: gettext('NVIDIA GPU Status'), 246 | iconCls: 'fa fa-fw fa-television', 247 | textField: 'nvidiaGpuOutput', 248 | renderer: function(value) { 249 | if (!value || value.trim() === '') { 250 | return '
No NVIDIA GPUs detected or nvidia-smi not available
'; 251 | } 252 | 253 | // Temperature conversion settings 254 | WIDGET_EOF 255 | 256 | # Add temperature settings based on user selection 257 | if [[ "$TEMP_UNIT" == "F" ]]; then 258 | cat >> "$temp_js_file" << 'WIDGET_EOF' 259 | var toFahrenheit = true; 260 | var tempUnit = '°F'; 261 | var tempWarning = 158; // 70°C in F 262 | var tempCritical = 185; // 85°C in F 263 | WIDGET_EOF 264 | else 265 | cat >> "$temp_js_file" << 'WIDGET_EOF' 266 | var toFahrenheit = false; 267 | var tempUnit = '°C'; 268 | var tempWarning = 70; 269 | var tempCritical = 85; 270 | WIDGET_EOF 271 | fi 272 | 273 | # Continue with the rest of the widget code 274 | cat >> "$temp_js_file" << 'WIDGET_EOF' 275 | 276 | function convertTemp(celsius) { 277 | if (toFahrenheit) { 278 | return (celsius * 9 / 5) + 32; 279 | } 280 | return celsius; 281 | } 282 | 283 | function formatTemp(celsius) { 284 | var temp = convertTemp(celsius); 285 | var style = ''; 286 | var convertedWarning = toFahrenheit ? tempWarning : 70; 287 | var convertedCritical = toFahrenheit ? tempCritical : 85; 288 | 289 | if (temp >= convertedCritical) { 290 | style = 'color: #ff4444; font-weight: bold;'; 291 | } else if (temp >= convertedWarning) { 292 | style = 'color: #FFC300; font-weight: bold;'; 293 | } 294 | return '' + temp.toFixed(0) + tempUnit + ''; 295 | } 296 | 297 | function formatMemory(used, total) { 298 | var percent = (used / total * 100).toFixed(1); 299 | var style = ''; 300 | if (percent >= 90) { 301 | style = 'color: #ff4444; font-weight: bold;'; 302 | } else if (percent >= 75) { 303 | style = 'color: #FFC300;'; 304 | } 305 | return '' + used.toLocaleString() + ' of ' + total.toLocaleString() + ' MiB (' + percent + '%)'; 306 | } 307 | 308 | function formatPower(draw, limit) { 309 | // Handle NaN power draw (some GPUs don't report power) 310 | if (isNaN(draw)) { 311 | draw = 0; 312 | } 313 | if (isNaN(limit) || limit === 0) { 314 | return '' + draw.toFixed(0) + 'W'; 315 | } 316 | var percent = (draw / limit * 100); 317 | var style = ''; 318 | if (percent >= 90) { 319 | style = 'color: #ff4444; font-weight: bold;'; 320 | } else if (percent >= 75) { 321 | style = 'color: #FFC300;'; 322 | } 323 | return '' + draw.toFixed(0) + ' of ' + limit.toFixed(0) + 'W'; 324 | } 325 | 326 | function formatUtilization(util) { 327 | var style = ''; 328 | if (util >= 90) { 329 | style = 'color: #FFC300;'; 330 | } 331 | return '' + util + '%'; 332 | } 333 | 334 | function formatFan(fan) { 335 | if (fan === null || fan === undefined || isNaN(fan) || fan === '[Not Supported]' || fan === '') { 336 | return 'N/A'; 337 | } 338 | return fan + '%'; 339 | } 340 | 341 | var lines = value.trim().split('\n'); 342 | var result = []; 343 | 344 | for (var i = 0; i < lines.length; i++) { 345 | var line = lines[i].trim(); 346 | if (!line) continue; 347 | 348 | var parts = line.split(',').map(function(p) { return p.trim(); }); 349 | 350 | if (parts.length < 10) continue; 351 | 352 | var gpuIndex = parts[0]; 353 | var gpuName = parts[1]; 354 | var temp = parseFloat(parts[2]); 355 | var gpuUtil = parseInt(parts[3], 10); 356 | var memUtil = parseInt(parts[4], 10); 357 | var memUsed = parseFloat(parts[5]); 358 | var memTotal = parseFloat(parts[6]); 359 | var powerDraw = parseFloat(parts[7]); 360 | var powerLimit = parseFloat(parts[8]); 361 | var fanSpeed = parts[9]; 362 | 363 | // Parse fan speed (may be [Not Supported] or a number) 364 | var fanValue = parseFloat(fanSpeed); 365 | if (isNaN(fanValue)) { 366 | fanValue = null; 367 | } 368 | 369 | var gpuHtml = '
'; 370 | gpuHtml += '
GPU ' + gpuIndex + ': ' + gpuName + '
'; 371 | gpuHtml += '
'; 372 | gpuHtml += 'Temp: ' + formatTemp(temp); 373 | gpuHtml += '  |  GPU: ' + formatUtilization(gpuUtil); 374 | gpuHtml += '  |  Mem: ' + formatMemory(memUsed, memTotal); 375 | gpuHtml += '  |  Power: ' + formatPower(powerDraw, powerLimit); 376 | gpuHtml += '  |  Fan: ' + formatFan(fanValue); 377 | gpuHtml += '
'; 378 | 379 | result.push(gpuHtml); 380 | } 381 | 382 | if (result.length === 0) { 383 | return '
No GPU data available
'; 384 | } 385 | 386 | return '
' + result.join('') + '
'; 387 | } 388 | }, 389 | WIDGET_EOF 390 | 391 | if [[ $? -ne 0 ]]; then 392 | err "Failed to generate GPU widget code" 393 | fi 394 | 395 | # Insert the widget BEFORE the 'cpus' item 396 | # We use perl for reliable multi-line pattern matching and insertion 397 | # The BEGIN block reads the widget file before processing the main file 398 | perl -i -0777 -pe ' 399 | BEGIN { 400 | open(my $fh, "<", "/tmp/nvidia_gpu_widget.js") or die "Cannot open widget file: $!"; 401 | local $/; 402 | $::widget = <$fh>; 403 | close($fh); 404 | } 405 | # Find the cpus item within StatusView and insert widget before it 406 | s/(Ext\.define\('\''PVE\.node\.StatusView'\''.*?items:\s*\[.*?)({\s*itemId:\s*'\''cpus'\'')/$1$::widget$2/s; 407 | ' "$PVE_MANAGER_LIB_JS_FILE" 408 | 409 | local insert_status=$? 410 | 411 | # Verify the insertion succeeded 412 | if [[ $insert_status -ne 0 ]]; then 413 | rm -f "$temp_js_file" 414 | err "Failed to insert GPU widget into pvemanagerlib.js (perl error)" 415 | fi 416 | 417 | # Verify the widget was actually inserted by checking for our itemId 418 | if ! grep -q "itemId: 'nvidiaGpu'" "$PVE_MANAGER_LIB_JS_FILE"; then 419 | rm -f "$temp_js_file" 420 | err "Widget insertion verification failed - nvidiaGpu itemId not found in file" 421 | fi 422 | 423 | rm -f "$temp_js_file" 424 | info "GPU widget inserted into \"$PVE_MANAGER_LIB_JS_FILE\"." 425 | } 426 | 427 | # Main installation function 428 | function install_mod() { 429 | msgb "\n=== Preparing NVIDIA GPU mod installation ===" 430 | 431 | check_root_privileges 432 | check_mod_installation 433 | configure 434 | perform_backup 435 | 436 | insert_node_info 437 | insert_gpu_widget 438 | 439 | msgb "\n=== Finalizing installation ===" 440 | 441 | restart_proxy 442 | 443 | info "Installation completed successfully." 444 | msgb "\nIMPORTANT: Clear your browser cache (Ctrl+Shift+R) to see the changes." 445 | } 446 | 447 | # Uninstall the modification 448 | function uninstall_mod() { 449 | msgb "\n=== Uninstalling NVIDIA GPU Mod ===" 450 | 451 | check_root_privileges 452 | 453 | # Check if mod is installed 454 | if ! grep -q 'nvidiaGpuOutput' "$NODES_PM_FILE" 2>/dev/null; then 455 | err "NVIDIA GPU mod is not installed." 456 | fi 457 | 458 | set_backup_directory 459 | 460 | # Check for other mods that would be affected by backup restoration 461 | local other_mods_detected=false 462 | local detected_mods="" 463 | 464 | if grep -q 'sensorsOutput' "$NODES_PM_FILE" 2>/dev/null; then 465 | other_mods_detected=true 466 | detected_mods="pve-mod-gui-sensors" 467 | fi 468 | 469 | if [[ "$other_mods_detected" == true ]]; then 470 | warn "Other PVE mods detected: $detected_mods" 471 | warn "Restoring from backup will remove ALL mods installed after the nvidia-gpu backup was created." 472 | msgb "\nYou have two options:" 473 | echo " 1) Continue - Restore backup, then reinstall other mods afterward" 474 | echo " 2) Cancel - Manually remove nvidia-gpu code from files instead" 475 | echo "" 476 | local confirm 477 | confirm=$(ask "Continue with backup restoration? (y/N)") 478 | if [[ ! "$confirm" =~ ^[yY]$ ]]; then 479 | info "Uninstall cancelled." 480 | msgb "\nTo manually remove, edit these files:" 481 | echo " - $NODES_PM_FILE (remove nvidiaGpuOutput lines)" 482 | echo " - $PVE_MANAGER_LIB_JS_FILE (remove nvidiaGpu widget)" 483 | exit 0 484 | fi 485 | fi 486 | 487 | info "Restoring modified files..." 488 | 489 | # Find the latest Nodes.pm backup 490 | local latest_nodes_pm 491 | latest_nodes_pm=$(find "$BACKUP_DIR" -name "nvidia-gpu.Nodes.pm.*" -type f -printf '%T+ %p\n' 2>/dev/null | sort -r | head -n 1 | awk '{print $2}') 492 | 493 | if [[ -n "$latest_nodes_pm" ]]; then 494 | msgb "Restoring Nodes.pm from backup: $latest_nodes_pm" 495 | cp "$latest_nodes_pm" "$NODES_PM_FILE" 496 | info "Restored Nodes.pm successfully." 497 | else 498 | warn "No Nodes.pm backup found. Attempting manual removal..." 499 | # Remove the nvidia-smi lines manually 500 | sed -i '/# Collect NVIDIA GPU data/,/nvidiaGpuOutput.*nvidia-smi/d' "$NODES_PM_FILE" 501 | fi 502 | 503 | # Find the latest pvemanagerlib.js backup 504 | local latest_pvemanagerlibjs 505 | latest_pvemanagerlibjs=$(find "$BACKUP_DIR" -name "nvidia-gpu.pvemanagerlib.js.*" -type f -printf '%T+ %p\n' 2>/dev/null | sort -r | head -n 1 | awk '{print $2}') 506 | 507 | if [[ -n "$latest_pvemanagerlibjs" ]]; then 508 | msgb "Restoring pvemanagerlib.js from backup: $latest_pvemanagerlibjs" 509 | cp "$latest_pvemanagerlibjs" "$PVE_MANAGER_LIB_JS_FILE" 510 | info "Restored pvemanagerlib.js successfully." 511 | else 512 | warn "No pvemanagerlib.js backup found. Manual restoration may be required." 513 | warn "You can reinstall pve-manager package to restore: apt install --reinstall pve-manager" 514 | fi 515 | 516 | restart_proxy 517 | 518 | info "Uninstallation completed." 519 | msgb "\nIMPORTANT: Clear your browser cache (Ctrl+Shift+R) to see the changes." 520 | } 521 | 522 | # Process command line arguments 523 | executed=0 524 | while [[ $# -gt 0 ]]; do 525 | case "$1" in 526 | install) 527 | executed=$((executed + 1)) 528 | install_mod 529 | ;; 530 | uninstall) 531 | executed=$((executed + 1)) 532 | uninstall_mod 533 | ;; 534 | *) 535 | warn "Unknown option: $1" 536 | usage 537 | ;; 538 | esac 539 | shift 540 | done 541 | 542 | # If no arguments provided, show usage 543 | if [[ $executed -eq 0 ]]; then 544 | usage 545 | fi 546 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------