├── pve-mod-sensors.png
├── pve-mod-all.sh
├── .github
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── .gitignore
├── security.md
├── updateallcontainers.sh
├── pve-mod-nag-screen.sh
├── readme.md
└── pve-mod-gui-sensors.sh
/pve-mod-sensors.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Meliox/PVE-mods/HEAD/pve-mod-sensors.png
--------------------------------------------------------------------------------
/pve-mod-all.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # This script's working directory
4 | SCRIPT_CWD="$(dirname "$(readlink -f "$0")")"
5 |
6 | MODS="pve-mod-gui-sensors.sh pve-mod-nag-screen.sh"
7 |
8 | ACTION=${1:-install}
9 | for m in $MODS; do
10 | "$SCRIPT_CWD/$m" $ACTION
11 | done
12 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Sensors output**
27 | If applicable, provide sensor datadump (use the argument save-sensors-data)
28 |
29 | **Additional context**
30 | Add any other context about the problem here.
31 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
22 | **Sensors output**
23 | If applicable, provide sensor datadump (use the argument save-sensors-data)
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ################################################################################
2 | # This .gitignore file was automatically created by Microsoft(R) Visual Studio.
3 | ################################################################################
4 |
5 | /.vs/PVE-mods/FileContentIndex/b0cd8cf0-a894-4523-bdd5-7f3d85072f7f.vsidx
6 | /.vs/PVE-mods/FileContentIndex/c351d5b4-2706-444a-87b2-a910a8873e48.vsidx
7 | /.vs/PVE-mods/FileContentIndex/cd6884c2-daf4-47a9-8bbd-cf8a4f886075.vsidx
8 | /.vs/PVE-mods/FileContentIndex/read.lock
9 | /.vs/PVE-mods/v17
10 | /.vs/PVE-mods/v17/.wsuo
11 | /.vs/slnx.sqlite
12 | /.vs/VSWorkspaceState.json
13 | /.vs/PVE-mods/FileContentIndex/8b60d584-2422-418a-808a-7a62a50425b3.vsidx
14 | /.vs/PVE-mods/FileContentIndex/aecfa7d2-4343-412e-a663-cd485991e27d.vsidx
15 | /.vs/PVE-mods/FileContentIndex/da25d4cb-f11b-4cdb-b231-55efa7695267.vsidx
16 | /.vs/PVE-mods/FileContentIndex
17 |
--------------------------------------------------------------------------------
/security.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | These scripts and modifications are designed for use with **Proxmox Virtual Environment (PVE)**.
6 |
7 | ⚠️ **Note:**
8 | - Proxmox upgrades may overwrite modified files. Reinstallation of the mods may be required.
9 | - Functionality on unsupported versions is not guaranteed.
10 |
11 | ---
12 |
13 | ## Security Considerations
14 |
15 | - **Root Access Required**:
16 | - All installation steps must be performed as `root`.
17 | - Normal users do not have access to required system paths.
18 | - Misuse may compromise your Proxmox installation.
19 | - **Backups**: Scripts automatically back up modified system files before applying changes.
20 | - **Network Security**:
21 | - UPS information requires network monitoring; secure communication is recommended.
22 | - Multi-node UPS support assumes identical credentials across nodes, which may pose risks if not properly secured.
23 |
24 | ---
25 |
26 | ## Reporting a Vulnerability
27 | If you discover a security issue in these scripts or modifications, please open an issue.
28 |
29 | ---
30 |
31 | ## Best Practices
32 | - Review code prior to installation
33 | - Always test on a **non-production node** before applying in production.
34 | - Keep a full **system backup and VM/container snapshots** before installing modifications.
35 | - Verify downloaded scripts from the official repo via checksum before execution.
36 | - After upgrades, re-check whether mods are still applied correctly.
37 | - Clear browser cache after applying GUI modifications.
38 |
39 | ---
40 |
41 | ## Disclaimer
42 | These scripts are provided **as-is**, without warranty of any kind.
43 | Use at your own risk. The author(s) are not responsible for data loss, downtime, or security breaches resulting from the use of these modifications.
--------------------------------------------------------------------------------
/updateallcontainers.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # This script updates all running Proxmox containers, skipping specified excluded containers, and generates a separate log file for each container.
3 | # The script first updates the Proxmox host system, then iterates through each container, updates the container, and reboots it if necessary.
4 | # Each container's log file is stored in $log_path and the main script log file is named container-upgrade-main.log.
5 |
6 | # Path where logs are saved
7 | log_path="/root/scripts"
8 |
9 | # array of container ids to exclude from updates
10 | exclude_containers=("106")
11 |
12 | # path to programs
13 | pct="/usr/sbin/pct"
14 |
15 | # list of container ids we need to iterate through
16 | containers=$($pct list | tail -n +2 | cut -f1 -d' ')
17 |
18 | #### CODE BELOW #########
19 | container_main_log_file="${log_path}/container-upgrade-main.log"
20 |
21 | echo "[Info] Updating proxmox containers at $(date)"
22 | echo "[Info] Updating proxmox containers at $(date)" >> $container_main_log_file
23 |
24 | #function to update individual containers
25 | function update_container() {
26 | container=$1
27 | # log file for individual container
28 | container_log_file="${log_path}/container-upgrade-$container.log"
29 |
30 | # log start of update
31 | echo "[Info] Starting update for container $container at $(date)" >> $container_log_file
32 |
33 | # perform the update
34 | $pct exec $container -- bash -c "apt update && apt upgrade -y && apt autoremove -y && reboot" >> $container_log_file 2>&1
35 |
36 | # log completion of update
37 | echo "[Info] Completed update for $container at $(date)" >> $container_log_file
38 | echo "--------------------------------------------------------------------------------------------" >> $container_log_file
39 | }
40 |
41 | for container in $containers; do
42 | # skip excluded containers
43 | if [[ " ${exclude_containers[@]} " =~ " ${container} " ]]; then
44 | echo "[Info] Skipping excluded container, $container"
45 | echo "[Info] Skipping excluded container, $container" >> $container_main_log_file
46 | continue
47 | fi
48 |
49 | status=$($pct status $container)
50 | if [ "$status" == "status: stopped" ]; then
51 | echo "[Info] Skipping offline container, $container"
52 | echo "[Info] Skipping offline container, $container" >> $container_main_log_file
53 | elif [ "$status" == "status: running" ]; then
54 | update_container $container
55 | fi
56 | done; wait
57 |
58 | # log completion of all updates
59 | echo "[Info] Updating proxmox containers completed at $(date)"
60 | echo "[Info] Updating proxmox containers completed at $(date)" >> $container_main_log_file
61 | echo "--------------------------------------------------------------------------------------------" >> $container_main_log_file
--------------------------------------------------------------------------------
/pve-mod-nag-screen.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | #
3 | # This bash script installs a modification to the Proxmox Virtual Environment (PVE) web user interface (UI) which deactivates the subscription nag screen.
4 | #
5 |
6 | ################### Configuration #############
7 |
8 | # This script's working directory
9 | SCRIPT_CWD="$(dirname "$(readlink -f "$0")")"
10 |
11 | # Files backup location
12 | BACKUP_DIR="$SCRIPT_CWD/backup"
13 |
14 | # File paths
15 | proxmoxlibjs="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
16 | proxmoxlibminjs="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js"
17 |
18 | ###############################################
19 |
20 | # Helper functions
21 | function msg {
22 | echo -e "\e[0m$1\e[0m"
23 | }
24 |
25 | #echo message in bold
26 | function msgb {
27 | echo -e "\e[1m$1\e[0m"
28 | }
29 |
30 | function warn {
31 | echo -e "\e[0;33m[warning] $1\e[0m"
32 | }
33 |
34 | function err {
35 | echo -e "\e[0;31m[error] $1\e[0m"
36 | exit 1
37 | }
38 | # End of helper functions
39 |
40 | # Function to display usage information
41 | function usage {
42 | msgb "\nUsage:\n$0 [install | uninstall]\n"
43 | exit 1
44 | }
45 |
46 | function restart_proxy {
47 | # Restart pveproxy
48 | msg "\nRestarting PVE proxy..."
49 | systemctl restart pveproxy
50 | }
51 |
52 | function install_mod {
53 | msg "\nPreparing mod installation..."
54 |
55 | local timestamp=$(date '+%Y-%m-%d_%H-%M-%S')
56 | local restart=false
57 |
58 | if (! grep -q "// disable subscription nag screen" "$proxmoxlibjs"); then
59 | if [ -f "$proxmoxlibjs" ]; then
60 | mkdir -p "$BACKUP_DIR" || err "Error creating backup directory."
61 |
62 | # Create backup of original file
63 | msg "Saving current version of \"$proxmoxlibjs\" to \"$BACKUP_DIR/proxmoxlib.js.$timestamp\"."
64 | cp -P "$proxmoxlibjs" "$BACKUP_DIR/proxmoxlib.js.$timestamp" || err "Error creating backup."
65 |
66 | msg "Deactivating the nag screen..."
67 | sed -i "/Ext.define('Proxmox.Utils',/ {
68 | :a;
69 | /checked_command:\s*function\s*(orig_cmd)\s*{/!{N;ba;}
70 | a\
71 | \\
72 | // disable subscription nag screen\n\
73 | orig_cmd();\n\
74 | return;
75 | }" "$proxmoxlibjs"
76 | fi
77 | restart=true
78 | else
79 | warn "Nag screen already deactivated."
80 | fi
81 |
82 | if [ ! -h "$proxmoxlibminjs" ]; then
83 | msg "Disabling minified front-end library file..."
84 | (mv "$proxmoxlibminjs" "$BACKUP_DIR/proxmoxlib.min.js.$timestamp" &&
85 | ln -s "$proxmoxlibjs" "$proxmoxlibminjs") || err "Error disabling minified front-end library file."
86 | restart=true
87 | else
88 | warn "Minified front-end library file already disabled."
89 | fi
90 |
91 | if [ $restart = true ]; then
92 | restart_proxy
93 | fi
94 | }
95 |
96 | function uninstall_mod {
97 | msg "\nRestoring modified files..."
98 |
99 | local restart=false
100 |
101 | # Find the latest backup file of proxmoxlib.js
102 | local latest_proxmoxlibjs=$(find "$BACKUP_DIR" -name "proxmoxlib.js.*" -type f -printf '%T+ %p\n' 2>/dev/null | sort -r | head -n 1 | awk '{print $2}')
103 |
104 | if [ -z "$latest_proxmoxlibjs" ]; then
105 | warn "No proxmoxlib.js backup files found."
106 | else
107 | # Remove the latest proxmoxlib.js file
108 | msg "Restoring \"$proxmoxlibjs\" from the latest backup file."
109 | cp "$latest_proxmoxlibjs" "$proxmoxlibjs"
110 | restart=true
111 | fi
112 |
113 | local latest_proxmoxlibminjs=$(find "$BACKUP_DIR" -name "proxmoxlib.min.js.*" -type f -printf '%T+ %p\n' 2>/dev/null | sort -r | head -n 1 | awk '{print $2}')
114 |
115 | if [ -z "$latest_proxmoxlibminjs" ]; then
116 | warn "No proxmoxlib.min.js backup files found."
117 | else
118 | # Remove the latest proxmoxlib.min.js file
119 | msg "Restoring \"$proxmoxlibminjs\" from the latest backup file."
120 | rm "$proxmoxlibminjs" && cp "$latest_proxmoxlibminjs" "$proxmoxlibminjs"
121 | restart=true
122 | fi
123 |
124 | if [ $restart = true ]; then
125 | restart_proxy
126 | fi
127 | }
128 |
129 | # Process the arguments using a while loop and a case statement
130 | executed=0
131 | while [[ $# -gt 0 ]]; do
132 | case "$1" in
133 | install)
134 | executed=$(($executed + 1))
135 | msgb "\nInstalling the Proxmox VE nag screen mod..."
136 | install_mod
137 | echo # add a new line
138 | ;;
139 | uninstall)
140 | executed=$(($executed + 1))
141 | msgb "\nUninstalling the Proxmox VE nag screen mod..."
142 | uninstall_mod
143 | echo # add a new line
144 | ;;
145 | esac
146 | shift
147 | done
148 |
149 | # If no arguments were provided or all arguments have been processed, print the usage message
150 | if [[ $executed -eq 0 ]]; then
151 | usage
152 | fi
153 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Proxmox Virtual Environment mods and scripts
2 | A small collection of scripts and mods for Proxmox Virtual Environment (PVE)
3 |
4 | If you find this helpful, a small donation is appreciated, [](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 | 
9 |
10 | This bash script installs a modification to the Proxmox Virtual Environment (PVE) web user interface (UI) to display sensor readings in a flexible and readable manner.
11 | The following readings are possible:
12 | - CPU, NVMe/HDD/SSD temperatures (Celsius/Fahrenheit), fan speeds, ram temperatures via lm-sensors. Note: Hdds require kernel module *drivetemp* module installed.
13 | - UPS information via Network Monitoring Tool
14 | - Motherboard information or system information via dmidecode
15 |
16 | ### How it works
17 | The modification involves the following steps:
18 | 1. Backup original files in the home directory/backup
19 | - `/usr/share/pve-manager/js/pvemanagerlib.js`
20 | - `/usr/share/perl5/PVE/API2/Nodes.pm`
21 | 2. Patch `Nodes.pm` to enable readings.
22 | 3. Modify `pvemanagerlib.js` to:
23 | - Expand the node status view to full browser width.
24 | - Add reading (depending on & selections).
25 | - Allow collapsing the panel vertically.
26 | 4. Restart the `pveproxy` service to apply changes.
27 |
28 | The script provides three options:
29 | | **Option** | **Description** |
30 | |-------------------------|-----------------------------------------------------------------------------|
31 | | `install` | Apply the modification. |
32 | | `uninstall` | Restore original files from backups. |
33 | | `save-sensors-data` | Save a local copy of detected sensor data for reference or troubleshooting. |
34 |
35 | Notes:
36 | - UPS support in multi-node setups require identical login credentials across nodes. This has not been fully tested.
37 | - Proxmox upgrades may overwrite modified files; reinstallation of this mod could be required.
38 |
39 | ### Install
40 | Instructions be performed as 'root', as normal users do not have access to the files.
41 |
42 | ```
43 | apt-get install lm-sensors
44 | # lm-sensors must be configured, run below to configure your sensors, apply temperature offsets. Refer to lm-sensors manual for more information.
45 | sensors-detect
46 | wget https://raw.githubusercontent.com/Meliox/PVE-mods/refs/heads/main/pve-mod-gui-sensors.sh
47 | bash pve-mod-gui-sensors.sh install
48 | # Then clear the browser cache to ensure all changes are visualized.
49 | ```
50 | Additionally, adjustments are available in the first part of the script, where paths can be edited, cpucore offset and display information.
51 |
52 | ## Nag screen deactivation
53 | (Tested compatibility: 7.x - 8.3.5)
54 | This bash script installs a modification to the Proxmox Virtual Environment (PVE) web user interface (UI) which deactivates the subscription nag screen.
55 |
56 | The modification includes two main steps:
57 | 1. Create backups of the original files in the `backup` directory relative to the script location.
58 | 2. Modify code.
59 |
60 | The script provides three options:
61 | | **Option** | **Description** |
62 | |-------------------------|-----------------------------------------------------------------------------|
63 | | `install` | Installs the modification by applying the necessary changes. |
64 | | `uninstall` | Removes the modification by restoring the original files from backups. |
65 |
66 | ### Install
67 | Instructions be performed as 'root', as normal users do not have access to the files.
68 | ```
69 | wget https://raw.githubusercontent.com/Meliox/PVE-mods/refs/heads/main/pve-mod-nag-screen.sh
70 | bash pve-mod-nag-screen.sh install
71 | ```
72 |
73 | ## Script to update all containers
74 | (Tested compatibility: 7.x - 8.3.5)
75 |
76 | This script updates all running Proxmox containers, skipping specified excluded containers, and generates a separate log file for each container.
77 | The script first updates the Proxmox host system, then iterates through each container, updates the container, and reboots it if necessary.
78 | Each container's log file is stored in $log_path and the main script log file is named container-upgrade-main.log.
79 |
80 | ### Install
81 | ```
82 | wget https://raw.githubusercontent.com/Meliox/PVE-mods/refs/heads/main/updateallcontainers.sh
83 | ```
84 | Or use git clone.
85 | Can be added to cron for e.g. monthly update: ```0 6 1 * * /root/scripts/updateallcontainers.sh```
86 |
--------------------------------------------------------------------------------
/pve-mod-gui-sensors.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | #
3 | # This bash script installs a modification to the Proxmox Virtual Environment (PVE) web user interface (UI) to display sensors information.
4 | #
5 |
6 | ################### Configuration #############
7 |
8 | # Display configuration for HDD, NVME, CPU
9 | # Set to 0 to disable line breaks
10 | # Note: use these settings only if the displayed layout is broken
11 | CPU_ITEMS_PER_ROW=0
12 | NVME_ITEMS_PER_ROW=0
13 | HDD_ITEMS_PER_ROW=0
14 |
15 | # Known CPU sensor names. They can be full or partial but should ensure unambiguous identification.
16 | # Should new ones be added, also update logic in configure() function.
17 | KNOWN_CPU_SENSORS=("coretemp-isa-" "k10temp-pci-")
18 |
19 | # Overwrite default backup location
20 | BACKUP_DIR=""
21 |
22 | ##################### DO NOT EDIT BELOW #######################
23 | # Only to be used to debug on other systems. Save the "sensor -j" output into a json file.
24 | # Information will be loaded for script configuration and presented in Proxmox.
25 |
26 | # DEV NOTE: lm-sensors version >3.6.0 breakes properly formatted JSON output using 'sensors -j'. This implements a workaround using uses a python3 for formatting
27 |
28 | DEBUG_REMOTE=false
29 | DEBUG_JSON_FILE="/tmp/sensordata.json"
30 | DEBUG_UPS_FILE="/tmp/upsc.txt"
31 |
32 | # This script's working directory
33 | SCRIPT_CWD="$(dirname "$(readlink -f "$0")")"
34 |
35 | # Debug location
36 | JSON_EXPORT_DIRECTORY="$SCRIPT_CWD"
37 | JSON_EXPORT_FILENAME="sensorsdata.json"
38 |
39 | # File paths
40 | PVE_MANAGER_LIB_JS_FILE="/usr/share/pve-manager/js/pvemanagerlib.js"
41 | NODES_PM_FILE="/usr/share/perl5/PVE/API2/Nodes.pm"
42 |
43 | #region message tools
44 | # Section header (bold)
45 | function msgb() {
46 | local message="$1"
47 | echo -e "\e[1m${message}\e[0m"
48 | }
49 |
50 | # Info (green)
51 | function info() {
52 | local message="$1"
53 | echo -e "\e[0;32m[info] ${message}\e[0m"
54 | }
55 |
56 | # Warning (yellow)
57 | function warn() {
58 | local message="$1"
59 | echo -e "\e[0;33m[warning] ${message}\e[0m"
60 | }
61 |
62 | # Error (red)
63 | function err() {
64 | local message="$1"
65 | echo -e "\e[0;31m[error] ${message}\e[0m"
66 | exit 1
67 | }
68 |
69 | # Prompts (cyan or bold)
70 | function ask() {
71 | local prompt="$1"
72 | local response
73 | read -p $'\n\e[1;36m'"${prompt}:"$'\e[0m ' response
74 | echo "$response"
75 | }
76 | #endregion message tools
77 |
78 | # Function to display usage information
79 | function usage {
80 | msgb "\nUsage:\n$0 [install | uninstall | save-sensors-data]\n"
81 | exit 1
82 | }
83 |
84 | # System checks
85 | function check_root_privileges() {
86 | [[ $EUID -eq 0 ]] || err "This script must be run as root. Please run it with 'sudo $0'."
87 | info "Root privileges verified."
88 | }
89 |
90 | # Define a function to install packages
91 | function install_packages {
92 | # Check if the 'sensors' command is available on the system
93 | if (! command -v sensors &>/dev/null); then
94 | # If the 'sensors' command is not available, prompt the user to install lm-sensors
95 | local choiceInstallLmSensors=$(ask "lm-sensors is not installed. Would you like to install it? (y/n)")
96 | case "$choiceInstallLmSensors" in
97 | [yY])
98 | # If the user chooses to install lm-sensors, update the package list and install the package
99 | apt-get update
100 | apt-get install lm-sensors
101 | ;;
102 | [nN])
103 | # If the user chooses not to install lm-sensors, exit the script with a zero status code
104 | msgb "Decided to not install lm-sensors. The mod cannot run without it. Exiting..."
105 | err "lm-sensors is required. Exiting..."
106 | ;;
107 | *)
108 | # If the user enters an invalid input, print an error message and exit the script with a non-zero status code
109 | err "Invalid input. Exiting..."
110 | ;;
111 | esac
112 | fi
113 |
114 | # Check if lm-sensors is installed correctly and exit if not
115 | if (! command -v sensors &>/dev/null); then
116 | err "lm-sensors installation failed or 'sensors' command is not available. Please install lm-sensors manually and re-run the script."
117 | fi
118 | }
119 |
120 | function configure {
121 | SENSORS_DETECTED=false
122 | local sensorsOutput
123 | local sanitisedSensorsOutput
124 | local upsOutput
125 | local modelName
126 |
127 | install_packages
128 |
129 | #### Collect lm-sensors output ####
130 | #region sensors collection
131 | if [ "$DEBUG_REMOTE" = true ]; then
132 | warn "Remote debugging is used. Sensor readings from dump file $DEBUG_JSON_FILE will be used."
133 | warn "Remote debugging is used. UPS readings from dump file $DEBUG_UPS_FILE will be used."
134 | sensorsOutput=$(cat "$DEBUG_JSON_FILE")
135 | else
136 | sensorsOutput=$(sensors -j 2>/dev/null)
137 | fi
138 |
139 | # Apply lm-sensors sanitization
140 | sanitisedSensorsOutput=$(sanitize_sensors_output "$sensorsOutput")
141 |
142 | if [ $? -ne 0 ]; then
143 | err "Sensor output error.\n\nCommand output:\n${sanitisedSensorsOutput}\n\nExiting..."
144 | fi
145 | #endregion sensors collection
146 |
147 | #### CPU ####
148 | #region cpu setup
149 | msgb "\n=== Detecting CPU temperature sensors ==="
150 | ENABLE_CPU=false
151 | local cpuList=""
152 | local cpuCount=0
153 |
154 | # Find all CPU sensors that match known patterns
155 | for pattern in "${KNOWN_CPU_SENSORS[@]}"; do
156 | found_sensors=$(echo "$sanitisedSensorsOutput" | grep -o "\"${pattern}[^\"]*\"" | sed 's/"//g')
157 | if [ -n "$found_sensors" ]; then
158 | while read -r sensor; do
159 | if [ -n "$sensor" ]; then
160 | cpuCount=$((cpuCount + 1))
161 | if [ -z "$cpuList" ]; then
162 | cpuList="$sensor"
163 | else
164 | cpuList="$cpuList,$sensor"
165 | fi
166 | ENABLE_CPU=true
167 | fi
168 | done <<< "$found_sensors"
169 | fi
170 | done
171 |
172 | if [ "$ENABLE_CPU" = true ]; then
173 | info "Detected CPU sensors ($cpuCount): $cpuList"
174 | SENSORS_DETECTED=true
175 | while true; do
176 | local choice=$(ask "Display temperatures for all cores [C] or average per CPU [a] (some newer AMD variants support per die)? (C/a)")
177 | case "$choice" in
178 | [cC]|"")
179 | CPU_TEMP_TARGET="Core"
180 | info "Temperatures will be displayed for all cores."
181 | break
182 | ;;
183 | [aA])
184 | CPU_TEMP_TARGET="Package"
185 | info "An average temperature will be displayed per CPU."
186 | break
187 | ;;
188 | *)
189 | warn "Invalid input, please choose C or a."
190 | ;;
191 | esac
192 | done
193 | else
194 | warn "No CPU temperature sensors found."
195 | fi
196 | #endregion cpu setup
197 |
198 | #### RAM ####
199 | #region ram setup
200 | msgb "\n=== Detecting RAM temperature sensors ==="
201 | local ramList=$(echo "$sanitisedSensorsOutput" | grep -o '"SODIMM[^"]*"' | sed 's/"//g' | paste -sd, -)
202 | local ramCount=$(grep -c '"SODIMM[^"]*"' <<<"$sanitisedSensorsOutput")
203 |
204 | if [ "$ramCount" -gt 0 ]; then
205 | info "Detected RAM sensors ($ramCount): $ramList"
206 | ENABLE_RAM_TEMP=true
207 | SENSORS_DETECTED=true
208 | else
209 | warn "No RAM temperature sensors found."
210 | ENABLE_RAM_TEMP=false
211 | fi
212 | #endregion ram setup
213 |
214 | #### HDD/SSD ####
215 | #region hdd setup
216 | msgb "\n=== Detecting HDD/SSD temperature sensors ==="
217 | local hddList=($(echo "$sanitisedSensorsOutput" | grep -o '"drivetemp-scsi[^"]*"' | sed 's/"//g'))
218 | if [ ${#hddList[@]} -gt 0 ]; then
219 | info "Detected HDD/SSD sensors (${#hddList[@]}): $(IFS=,; echo "${hddList[*]}")"
220 | ENABLE_HDD_TEMP=true
221 | SENSORS_DETECTED=true
222 | else
223 | warn "No HDD/SSD temperature sensors found."
224 | ENABLE_HDD_TEMP=false
225 | fi
226 | #endregion hdd setup
227 |
228 | #### NVMe ####
229 | #region nvme setup
230 | msgb "\n=== Detecting NVMe temperature sensors ==="
231 | local nvmeList=($(echo "$sanitisedSensorsOutput" | grep -o '"nvme[^"]*"' | sed 's/"//g'))
232 | if [ ${#nvmeList[@]} -gt 0 ]; then
233 | info "Detected NVMe sensors (${#nvmeList[@]}): $(IFS=,; echo "${nvmeList[*]}")"
234 | ENABLE_NVME_TEMP=true
235 | SENSORS_DETECTED=true
236 | else
237 | warn "No NVMe temperature sensors found."
238 | ENABLE_NVME_TEMP=false
239 | fi
240 | #endregion nvme setup
241 |
242 | #### Fans ####
243 | #region fan setup
244 | msgb "\n=== Detecting fan speed sensors ==="
245 |
246 | local fanList=""
247 | local fanCount=0
248 |
249 | # Find all fan names that have fan*_input entries
250 | fanList=$(echo "$sanitisedSensorsOutput" | grep -B2 '"fan[0-9]\+_input"' | grep '".*": {' | sed 's/.*"\([^"]*\)": {.*/\1/' | sort -u | paste -sd, -)
251 | fanCount=$(grep -c 'fan[0-9]\+_input' <<<"$sanitisedSensorsOutput")
252 |
253 | if [ "$fanCount" -gt 0 ]; then
254 | info "Detected fan speed sensors ($fanCount): $fanList"
255 | ENABLE_FAN_SPEED=true
256 | SENSORS_DETECTED=true
257 |
258 | local choice
259 | choice=$(ask "Display fans reporting zero speed? (Y/n)")
260 | case "$choice" in
261 | [yY]|"")
262 | DISPLAY_ZERO_SPEED_FANS=true
263 | info "Zero-speed fans will be displayed."
264 | ;;
265 | [nN])
266 | DISPLAY_ZERO_SPEED_FANS=false
267 | info "Only active fans will be displayed."
268 | ;;
269 | *)
270 | warn "Invalid input. Defaulting to show zero-speed fans."
271 | DISPLAY_ZERO_SPEED_FANS=true
272 | ;;
273 | esac
274 | else
275 | warn "No fan speed sensors found."
276 | ENABLE_FAN_SPEED=false
277 | fi
278 | #endregion fan setup
279 |
280 | #### Temperature Units ####
281 | #region temp unit setup
282 | msgb "\n=== Display temperature ==="
283 | if [ "$SENSORS_DETECTED" = true ]; then
284 | local unit=$(ask "Display temperatures in Celsius [C] or Fahrenheit [f]? (C/f)")
285 | case "$unit" in
286 | [cC]|"")
287 | TEMP_UNIT="C"
288 | info "Using Celsius."
289 | ;;
290 | [fF])
291 | TEMP_UNIT="F"
292 | info "Using Fahrenheit."
293 | ;;
294 | *)
295 | warn "Invalid selection. Defaulting to Celsius."
296 | TEMP_UNIT="C"
297 | ;;
298 | esac
299 | fi
300 | #endregion temp unit setup
301 |
302 | #### UPS ####
303 | #region ups setup
304 | local choiceUPS=$(ask "Enable UPS information? (y/N)")
305 | case "$choiceUPS" in
306 | [yY])
307 | if [ "$DEBUG_REMOTE" = true ]; then
308 | upsOutput=$(cat "$DEBUG_UPS_FILE")
309 | info "Remote debugging: UPS readings from $DEBUG_UPS_FILE"
310 | upsConnection="DEBUG_UPS"
311 | else
312 | upsConnection=$(ask "Enter UPS connection (e.g., upsname[@hostname[:port]])")
313 | if ! command -v upsc &>/dev/null; then
314 | err "The 'upsc' command is not available. Install 'nut-client'."
315 | fi
316 | upsOutput=$(upsc "$upsConnection" 2>&1)
317 | fi
318 |
319 | if echo "$upsOutput" | grep -q "device.model:"; then
320 | modelName=$(echo "$upsOutput" | grep "device.model:" | cut -d':' -f2- | xargs)
321 | ENABLE_UPS=true
322 | info "Connected to UPS model: $modelName at $upsConnection."
323 | else
324 | warn "Failed to connect to UPS at '$upsConnection'."
325 | ENABLE_UPS=false
326 | fi
327 | ;;
328 | [nN]|"")
329 | ENABLE_UPS=false
330 | info "UPS information will not be displayed."
331 | ;;
332 | *)
333 | warn "Invalid selection. UPS info will not be displayed."
334 | ENABLE_UPS=false
335 | ;;
336 | esac
337 | #endregion ups setup
338 |
339 | #### System Info ####
340 | #region system info setup
341 | msgb "\n=== Detecting System Information ==="
342 | for i in 1 2; do
343 | echo "type ${i})"
344 | dmidecode -t "$i" | awk -F': ' '/Manufacturer|Product Name|Serial Number/ {print $1": "$2}'
345 | done
346 | local choiceSysInfo=$(ask "Enable system information? (1/2/n)")
347 | case "$choiceSysInfo" in
348 | [1]|"")
349 | ENABLE_SYSTEM_INFO=true
350 | SYSTEM_INFO_TYPE=1
351 | info "System information will be displayed."
352 | ;;
353 | [2])
354 | ENABLE_SYSTEM_INFO=true
355 | SYSTEM_INFO_TYPE=2
356 | info "Motherboard information will be displayed."
357 | ;;
358 | [nN])
359 | ENABLE_SYSTEM_INFO=false
360 | info "System information will NOT be displayed."
361 | ;;
362 | *)
363 | warn "Invalid selection. Defaulting to system information."
364 | ENABLE_SYSTEM_INFO=true
365 | SYSTEM_INFO_TYPE=1
366 | ;;
367 | esac
368 | #endregion system info setup
369 |
370 | #### Final Check ####
371 | #region final check
372 | if [ "$SENSORS_DETECTED" = false ] && [ "$ENABLE_UPS" = false ] && [ "$ENABLE_SYSTEM_INFO" = false ]; then
373 | err "No sensors detected, UPS or system info enabled. Exiting."
374 | fi
375 | #endregion final check
376 | }
377 |
378 |
379 | # Function to install the modification
380 | function install_mod {
381 | local upsConnection
382 | msgb "\n=== Preparing mod installation ==="
383 | check_root_privileges
384 | check_mod_installation
385 | configure
386 | perform_backup
387 |
388 | #### Insert information retrieval code ####
389 | msgb "\n=== Inserting information retrieval code ==="
390 | insert_node_info
391 |
392 | #### Temperature helper parameters ####
393 | msgb "\n=== Creating temperature conversion helper ==="
394 | HELPERCTORPARAMS=$([[ "$TEMP_UNIT" = "F" ]] && \
395 | echo '{srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: PVE.mod.TempHelper.FAHRENHEIT}' || \
396 | echo '{srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: PVE.mod.TempHelper.CELSIUS}')
397 | info "Temperature helper configured for $TEMP_UNIT."
398 |
399 | #### Expand StatusView space ####
400 | expand_statusview_space
401 |
402 | #### Insert temperature helper ####
403 | generate_and_insert_temp_helper
404 |
405 | #### Generate and insert widgets ####
406 | msgb "\n=== Making visual adjustments ==="
407 |
408 | generate_and_insert_widget "$ENABLE_SYSTEM_INFO" "generate_system_info" "system_info"
409 | generate_and_insert_widget "$ENABLE_UPS" "generate_ups_widget" "ups"
410 | generate_and_insert_widget "$ENABLE_HDD_TEMP" "generate_hdd_widget" "hdd"
411 | generate_and_insert_widget "$ENABLE_NVME_TEMP" "generate_nvme_widget" "nvme"
412 |
413 | if [[ "$ENABLE_HDD_TEMP" = true || "$ENABLE_NVME_TEMP" = true ]]; then
414 | generate_drive_header
415 | info "Drive headers added."
416 | fi
417 |
418 | generate_and_insert_widget "$ENABLE_FAN_SPEED" "generate_fan_widget" "fan"
419 | generate_and_insert_widget "$ENABLE_RAM_TEMP" "generate_ram_widget" "ram"
420 | generate_and_insert_widget "$ENABLE_CPU" "generate_cpu_widget" "cpu"
421 |
422 | #### Visual separation ####
423 | add_visual_separator
424 | info "Added visual separator for modified items."
425 |
426 | #### Node summary ####
427 | setup_node_summary_container
428 | info "Node summary box moved into its own container."
429 |
430 | msgb "\n=== Finalizing installation ==="
431 |
432 | restart_proxy
433 | info "Installation completed."
434 | ask "Clear the browser cache to ensure all changes are visualized. (any key to continue)"
435 | }
436 |
437 | # Sanitize sensors output to handle common lm-sensors parsing issues
438 | sanitize_sensors_output() {
439 | local input="$1"
440 |
441 | # Pipe the text into Perl:
442 | # -0777 → "slurp mode": read the entire stream as one string so
443 | # regexes can match across line breaks.
444 | # -pe → loop over input, applying the script (-e) and printing.
445 | # Apply python3 json.tool for proper formatting and validation
446 | echo "$input" | perl -0777 -pe '
447 | # Replace ERROR lines with placeholder values
448 | s/ERROR:.+\s(\w+):\s(.+)/"$1": 0.000,/g;
449 | s/ERROR:.+\s(\w+)!/"$1": 0.000,/g;
450 |
451 | # Remove trailing commas before closing braces
452 | s/,\s*(\})/$1/g;
453 |
454 | # Replace NaN values with null
455 | s/\bNaN\b/null/g;
456 |
457 | # Fix duplicate SODIMM keys - handle both pretty and one-line JSON
458 | s/"SODIMM"\s*:\s*\{\s*"temp(\d+)_input"/"SODIMM $1": {\n "temp$1_input"/g;
459 |
460 | # Fix duplicate fan keys - handle both pretty and one-line JSON
461 | s/"([^"]*Fan[^"]*)"\s*:\s*\{\s*"fan(\d+)_input"/"$1 $2": {\n "fan$2_input"/g;
462 | ' | python3 -m json.tool 2>/dev/null || echo "$input"
463 | }
464 |
465 | #region node info insertion
466 | # Main insertion routine
467 | insert_node_info() {
468 | local output_file="$NODES_PM_FILE"
469 |
470 | collect_sensors_output "$output_file"
471 |
472 | if [[ $ENABLE_UPS == true ]]; then
473 | collect_ups_output "$output_file"
474 | fi
475 |
476 | if [[ $ENABLE_SYSTEM_INFO == true ]]; then
477 | collect_system_info "$output_file"
478 | fi
479 | }
480 |
481 | # Collect lm-sensors data
482 | collect_sensors_output() {
483 | local output_file="$1"
484 | local sensorsCmd
485 |
486 | if [[ $DEBUG_REMOTE == true ]]; then
487 | sensorsCmd="cat \"$DEBUG_JSON_FILE\""
488 | else
489 | # Note: sensors -f (Fahrenheit) breaks fan speeds
490 | sensorsCmd="sensors -j 2>/dev/null"
491 | fi
492 |
493 | # Remember to reflect this in sanitize_sensors_output()
494 | #region sensors heredoc
495 | sed -i '/my \$dinfo = df('\''\/'\'', 1);/i\
496 | # Collect sensor data from lm-sensors\
497 | $res->{sensorsOutput} = `'"$sensorsCmd"'`;\
498 | \
499 | # Sanitize JSON output to handle common lm-sensors parsing issues\
500 | # Replace ERROR lines with placeholder values\
501 | $res->{sensorsOutput} =~ s/ERROR:.+\\s(\\w+):\\s(.+)/\\"$1\\": 0.000,/g;\
502 | $res->{sensorsOutput} =~ s/ERROR:.+\\s(\\w+)!/\\"$1\\": 0.000,/g;\
503 | \
504 | # Remove trailing commas before closing braces\
505 | $res->{sensorsOutput} =~ s/,\\s*(\})/$1/g;\
506 | \
507 | # Replace NaN values with null for valid JSON\
508 | $res->{sensorsOutput} =~ s/\\bNaN\\b/null/g;\
509 | \
510 | # Fix duplicate SODIMM keys by appending temperature sensor number with a space - handle both pretty and one-line JSON\
511 | # Example: "SODIMM":{"temp3_input":34.0} becomes "SODIMM 3":{"temp3_input":34.0}\
512 | $res->{sensorsOutput} =~ s/"SODIMM"\\s*:\\s*\\{\\s*"temp(\\d+)_input"/"SODIMM $1": {\\n "temp$1_input"/g;\
513 | \
514 | # Fix duplicate fans keys by appending fan number with a space - handle both pretty and one-line JSON\
515 | # Example: "Processor Fan":{"fan2_input":1000,...} → "Processor Fan 2":{"fan2_input":1000,...}\
516 | $res->{sensorsOutput} =~ s/"([^"]+)"\\s*:\\s*\{\\s*"fan(\\d+)_input"/"$1 $2": {\\n "fan$2_input"/g;\
517 | \
518 | # Format JSON output properly (workaround for lm-sensors >3.6.0 issues)\
519 | $res->{sensorsOutput} =~ /^(.*)$/s;\
520 | $res->{sensorsOutput} = `echo \\Q$1\\E | python3 -m json.tool 2>/dev/null || echo \\Q$1\E`;\
521 | ' "$NODES_PM_FILE"
522 | #endregion sensors heredoc
523 | info "Sensors' retriever added to \"$output_file\"."
524 | }
525 |
526 | # Collect UPS data
527 | collect_ups_output() {
528 | local output_file="$1"
529 | local ups_cmd
530 |
531 | if [[ $DEBUG_REMOTE == true ]]; then
532 | ups_cmd="cat \"$DEBUG_UPS_FILE\""
533 | else
534 | ups_cmd="upsc \"$upsConnection\" 2>/dev/null"
535 | fi
536 |
537 | # region ups heredoc
538 | sed -i "/my \$dinfo = df('\/', 1);/i\\
539 | # Collect UPS status information\\
540 | sub get_upsc {\\
541 | my \$cmd = '$ups_cmd';\\
542 | my \$output = \`\\\$cmd\`;\\
543 | return \$output;\\
544 | }\\
545 | \$res->{upsc} = get_upsc();\\
546 | " "$NODES_PM_FILE"
547 | # endregion ups heredoc
548 |
549 | info "UPS retriever added to \"$output_file\"."
550 | }
551 |
552 |
553 | # Collect system information
554 | collect_system_info() {
555 | local output_file="$1"
556 | local systemInfoCmd
557 |
558 | systemInfoCmd=$(dmidecode -t "${SYSTEM_INFO_TYPE}" \
559 | | awk -F': ' '/Manufacturer|Product Name|Serial Number/ {print $1": "$2}' \
560 | | awk '{$1=$1};1' \
561 | | sed 's/$/ |/' \
562 | | paste -sd " " - \
563 | | sed 's/ |$//')
564 | #region system info heredoc
565 | sed -i "/my \$dinfo = df('\/', 1);/i\\
566 | # Add system information to response\\
567 | \$res->{systemInfo} = \"$(echo "$systemInfoCmd")\";\\
568 | " "$NODES_PM_FILE"
569 | #endregion system info heredoc
570 | info "System information retriever added to \"$output_file\"."
571 | }
572 | #endregion node info insertion
573 |
574 | #region widget generation functions
575 | # Helper function to insert widget after thermal items
576 | insert_widget_after_thermal() {
577 | local widget_file="$1"
578 | sed -i "/^Ext.define('PVE.node.StatusView',/ {
579 | :a
580 | /items:/!{N;ba;}
581 | :b
582 | /'cpus.*},/!{N;bb;}
583 | r $widget_file
584 | }" "$PVE_MANAGER_LIB_JS_FILE"
585 | }
586 |
587 | # Helper function to generate widget and insert it
588 | generate_and_insert_widget() {
589 | local enable_flag="$1"
590 | local generator_func="$2"
591 | local widget_name="$3"
592 |
593 | if [ "$enable_flag" = true ]; then
594 | local temp_js_file="/tmp/${widget_name}_widget.js"
595 | "$generator_func" "$temp_js_file"
596 | insert_widget_after_thermal "$temp_js_file"
597 | rm "$temp_js_file"
598 | info "Inserted $widget_name widget."
599 | fi
600 | }
601 |
602 | # Function to generate drive header
603 | generate_drive_header() {
604 | if [ "$ENABLE_NVME_TEMP" = true ] || [ "$ENABLE_HDD_TEMP" = true ]; then
605 | local temp_js_file="/tmp/drive_header.js"
606 | #region drive header heredoc
607 | cat > "$temp_js_file" <<'EOF'
608 | {
609 | xtype: 'box',
610 | colspan: 2,
611 | html: gettext('Drive(s)'),
612 | },
613 | EOF
614 | #endregion drive header heredoc
615 | if [[ $? -ne 0 ]]; then
616 | echo "Error: Failed to generate drive header code" >&2
617 | exit 1
618 | fi
619 |
620 | insert_widget_after_thermal "$temp_js_file"
621 | rm "$temp_js_file"
622 | fi
623 | }
624 |
625 | # Function to expand space and modify StatusView properties
626 | expand_statusview_space() {
627 | msgb "\n=== Expanding StatusView space ==="
628 |
629 | # Apply multiple modifications to the StatusView definition
630 | sed -i "/Ext.define('PVE\.node\.StatusView'/,/\},/ {
631 | s/\(bodyPadding:\) '[^']*'/\1 '20 15 20 15'/
632 | s/height: [0-9]\+/minHeight: 360,\n\tflex: 1,\n\tcollapsible: true,\n\ttitleCollapse: true/
633 | s/\(tableAttrs:.*$\)/trAttrs: \{ valign: 'top' \},\n\t\1/
634 | }" "$PVE_MANAGER_LIB_JS_FILE"
635 |
636 | if [[ $? -ne 0 ]]; then
637 | echo "Error: Failed to expand StatusView space" >&2
638 | exit 1
639 | fi
640 |
641 | info "Expanded space in \"$PVE_MANAGER_LIB_JS_FILE\"."
642 | }
643 |
644 | # Function to move node summary into its own container
645 | setup_node_summary_container() {
646 | # Move the node summary box into its own container
647 | local temp_js_file="/tmp/summary_container.js"
648 | #region summary container heredoc
649 | cat > "$temp_js_file" <<'EOF'
650 | {
651 | xtype: 'container',
652 | itemId: 'summarycontainer',
653 | layout: 'column',
654 | minWidth: 700,
655 | defaults: {
656 | minHeight: 350,
657 | padding: 5,
658 | columnWidth: 1,
659 | },
660 | items: [
661 | nodeStatus,
662 | ]
663 | },
664 | EOF
665 | #endregion summary container heredoc
666 | if [[ $? -ne 0 ]]; then
667 | echo "Error: Failed to generate summary container code" >&2
668 | exit 1
669 | fi
670 |
671 | # Insert the new container after finding the nodeStatus and items pattern
672 | sed -i "/^\s*nodeStatus: nodeStatus,/ {
673 | :a
674 | /items: \[/ !{N;ba;}
675 | r $temp_js_file
676 | }" "$PVE_MANAGER_LIB_JS_FILE"
677 |
678 | rm "$temp_js_file"
679 |
680 | # Deactivate the original box instance
681 | sed -i "/^\s*nodeStatus: nodeStatus,/ {
682 | :a
683 | /itemId: 'itemcontainer',/ !{N;ba;}
684 | n;
685 | :b
686 | /nodeStatus,/ !{N;bb;}
687 | s/nodeStatus/\/\/nodeStatus/
688 | }" "$PVE_MANAGER_LIB_JS_FILE"
689 |
690 | if [[ $? -ne 0 ]]; then
691 | echo "Error: Failed to deactivate original nodeStatus instance" >&2
692 | exit 1
693 | fi
694 | }
695 |
696 | # Function to add visual spacing separator after the last widget
697 | add_visual_separator() {
698 | # Check for the presence of items in the reverse order of display
699 | local lastItemId=""
700 |
701 | if [ "$ENABLE_UPS" = true ]; then
702 | lastItemId="upsc"
703 | elif [ "$ENABLE_HDD_TEMP" = true ]; then
704 | lastItemId="thermalHdd"
705 | elif [ "$ENABLE_NVME_TEMP" = true ]; then
706 | lastItemId="thermalNvme"
707 | elif [ "$ENABLE_FAN_SPEED" = true ]; then
708 | lastItemId="speedFan"
709 | else
710 | lastItemId="thermalCpu"
711 | fi
712 |
713 | if [ -n "$lastItemId" ]; then
714 | local temp_js_file="/tmp/visual_separator.js"
715 |
716 | #region visual spacing heredoc
717 | cat > "$temp_js_file" <<'EOF'
718 | {
719 | xtype: 'box',
720 | colspan: 2,
721 | padding: '0 0 20 0',
722 | },
723 | EOF
724 | #endregion visual spacing heredoc
725 | if [[ $? -ne 0 ]]; then
726 | echo "Error: Failed to generate visual separator code" >&2
727 | exit 1
728 | fi
729 |
730 | # Insert after the specific lastItemId (different pattern than thermal)
731 | sed -i "/^Ext.define('PVE.node.StatusView',/ {
732 | :a;
733 | /^.*{.*'$lastItemId'.*},/!{N;ba;}
734 | r $temp_js_file
735 | }" "$PVE_MANAGER_LIB_JS_FILE"
736 |
737 | rm "$temp_js_file"
738 | fi
739 | }
740 |
741 | # Function to generate system info widget
742 | generate_system_info() {
743 | #region system info heredoc
744 | cat > "$1" <<'EOF'
745 | {
746 | itemId: 'sysinfo',
747 | colspan: 2,
748 | printBar: false,
749 | title: gettext('System Information'),
750 | textField: 'systemInfo',
751 | renderer: function(value){
752 | return value;
753 | }
754 | },
755 | EOF
756 | #endregion system info heredoc
757 | if [[ $? -ne 0 ]]; then
758 | echo "Error: Failed to generate system info code" >&2
759 | exit 1
760 | fi
761 | }
762 |
763 | # Function to generate and insert temperature conversion helper class
764 | generate_and_insert_temp_helper() {
765 | local temp_js_file="/tmp/temp_helper.js"
766 |
767 | msgb "\n=== Inserting temperature helper ==="
768 |
769 | #region temp helper heredoc
770 | cat > "$temp_js_file" <<'EOF'
771 | Ext.define('PVE.mod.TempHelper', {
772 | //singleton: true,
773 |
774 | requires: ['Ext.util.Format'],
775 |
776 | statics: {
777 | CELSIUS: 0,
778 | FAHRENHEIT: 1
779 | },
780 |
781 | srcUnit: null,
782 | dstUnit: null,
783 |
784 | isValidUnit: function (unit) {
785 | return (
786 | Ext.isNumber(unit) && (unit === this.self.CELSIUS || unit === this.self.FAHRENHEIT)
787 | );
788 | },
789 |
790 | constructor: function (config) {
791 | this.srcUnit = config && this.isValidUnit(config.srcUnit) ? config.srcUnit : this.self.CELSIUS;
792 | this.dstUnit = config && this.isValidUnit(config.dstUnit) ? config.dstUnit : this.self.CELSIUS;
793 | },
794 |
795 | toFahrenheit: function (tempCelsius) {
796 | return Ext.isNumber(tempCelsius)
797 | ? tempCelsius * 9 / 5 + 32
798 | : NaN;
799 | },
800 |
801 | toCelsius: function (tempFahrenheit) {
802 | return Ext.isNumber(tempFahrenheit)
803 | ? (tempFahrenheit - 32) * 5 / 9
804 | : NaN;
805 | },
806 |
807 | getTemp: function (value) {
808 | if (this.srcUnit !== this.dstUnit) {
809 | switch (this.srcUnit) {
810 | case this.self.CELSIUS:
811 | switch (this.dstUnit) {
812 | case this.self.FAHRENHEIT:
813 | return this.toFahrenheit(value);
814 |
815 | default:
816 | Ext.raise({
817 | msg:
818 | 'Unsupported destination temperature unit: ' + this.dstUnit,
819 | });
820 | }
821 | case this.self.FAHRENHEIT:
822 | switch (this.dstUnit) {
823 | case this.self.CELSIUS:
824 | return this.toCelsius(value);
825 |
826 | default:
827 | Ext.raise({
828 | msg:
829 | 'Unsupported destination temperature unit: ' + this.dstUnit,
830 | });
831 | }
832 | default:
833 | Ext.raise({
834 | msg: 'Unsupported source temperature unit: ' + this.srcUnit,
835 | });
836 | }
837 | } else {
838 | return value;
839 | }
840 | },
841 |
842 | getUnit: function(plainText) {
843 | switch (this.dstUnit) {
844 | case this.self.CELSIUS:
845 | return plainText !== true ? '°C' : '\'C';
846 |
847 | case this.self.FAHRENHEIT:
848 | return plainText !== true ? '°F' : '\'F';
849 |
850 | default:
851 | Ext.raise({
852 | msg: 'Unsupported destination temperature unit: ' + this.srcUnit,
853 | });
854 | }
855 | },
856 | });
857 | EOF
858 | #endregion temp helper heredoc
859 | if [[ $? -ne 0 ]]; then
860 | echo "Error: Failed to generate temp helper code" >&2
861 | exit 1
862 | fi
863 |
864 | sed -i "/^Ext.define('PVE.node.StatusView'/e cat /tmp/temp_helper.js" "$PVE_MANAGER_LIB_JS_FILE"
865 | rm "$temp_js_file"
866 |
867 | info "Temperature helper inserted successfully."
868 | }
869 |
870 | # Function to generate CPU widget
871 | generate_cpu_widget() {
872 | #region cpu widget heredoc
873 | # use subshell to allow variable expansion
874 | (
875 | export CPU_ITEMS_PER_ROW
876 | export CPU_TEMP_TARGET
877 | export HELPERCTORPARAMS
878 |
879 | cat <<'EOF' | envsubst '$CPU_ITEMS_PER_ROW $CPU_TEMP_TARGET $HELPERCTORPARAMS' > "$1"
880 | {
881 | itemId: 'thermalCpu',
882 | colspan: 2,
883 | printBar: false,
884 | title: gettext('CPU Thermal State'),
885 | iconCls: 'fa fa-fw fa-thermometer-half',
886 | textField: 'sensorsOutput',
887 | renderer: function(value){
888 | // sensors configuration
889 | const cpuTempHelper = Ext.create('PVE.mod.TempHelper', $HELPERCTORPARAMS);
890 | // display configuration
891 | const itemsPerRow = $CPU_ITEMS_PER_ROW;
892 | // ---
893 | let objValue;
894 | try {
895 | objValue = JSON.parse(value) || {};
896 | } catch(e) {
897 | objValue = {};
898 | }
899 | const cpuKeysI = Object.keys(objValue).filter(item => String(item).startsWith('coretemp-isa-')).sort();
900 | const cpuKeysA = Object.keys(objValue).filter(item => String(item).startsWith('k10temp-pci-')).sort();
901 | const bINTEL = cpuKeysI.length > 0 ? true : false;
902 | const INTELPackagePrefix = '$CPU_TEMP_TARGET' == 'Core' ? 'Core ' : 'Package id';
903 | const INTELPackageCaption = '$CPU_TEMP_TARGET' == 'Core' ? 'Core' : 'Package';
904 | let AMDPackagePrefix = 'Tccd';
905 | let AMDPackageCaption = 'CCD';
906 |
907 | if (cpuKeysA.length > 0) {
908 | let bTccd = false;
909 | let bTctl = false;
910 | let bTdie = false;
911 | let bCpuCoreTemp = false;
912 | cpuKeysA.forEach((cpuKey, cpuIndex) => {
913 | let items = objValue[cpuKey];
914 | bTccd = Object.keys(items).findIndex(item => { return String(item).startsWith('Tccd'); }) >= 0;
915 | bTctl = Object.keys(items).findIndex(item => { return String(item).startsWith('Tctl'); }) >= 0;
916 | bTdie = Object.keys(items).findIndex(item => { return String(item).startsWith('Tdie'); }) >= 0;
917 | bCpuCoreTemp = Object.keys(items).findIndex(item => { return String(item) === 'CPU Core Temp'; }) >= 0;
918 | });
919 | if (bTccd && '$CPU_TEMP_TARGET' == 'Core') {
920 | AMDPackagePrefix = 'Tccd';
921 | AMDPackageCaption = 'ccd';
922 | } else if (bCpuCoreTemp && '$CPU_TEMP_TARGET' == 'Package') {
923 | AMDPackagePrefix = 'CPU Core Temp';
924 | AMDPackageCaption = 'CPU Core Temp';
925 | } else if (bTdie) {
926 | AMDPackagePrefix = 'Tdie';
927 | AMDPackageCaption = 'die';
928 | } else if (bTctl) {
929 | AMDPackagePrefix = 'Tctl';
930 | AMDPackageCaption = 'ctl';
931 | } else {
932 | AMDPackagePrefix = 'temp';
933 | AMDPackageCaption = 'Temp';
934 | }
935 | }
936 |
937 | const cpuKeys = bINTEL ? cpuKeysI : cpuKeysA;
938 | const cpuItemPrefix = bINTEL ? INTELPackagePrefix : AMDPackagePrefix;
939 | const cpuTempCaption = bINTEL ? INTELPackageCaption : AMDPackageCaption;
940 | const formatTemp = bINTEL ? '0' : '0.0';
941 | const cpuCount = cpuKeys.length;
942 | let temps = [];
943 |
944 | cpuKeys.forEach((cpuKey, cpuIndex) => {
945 | let cpuTemps = [];
946 | const items = objValue[cpuKey];
947 | const itemKeys = Object.keys(items).filter(item => {
948 | if ('$CPU_TEMP_TARGET' == 'Core') {
949 | // In Core mode: only show individual cores/CCDs, exclude overall CPU temp
950 | return String(item).includes(cpuItemPrefix) || String(item).startsWith('Tccd');
951 | } else {
952 | // In Package mode: show overall CPU temp and package-level readings
953 | return String(item).includes(cpuItemPrefix) || String(item) === 'CPU Core Temp';
954 | }
955 | });
956 |
957 | itemKeys.forEach((coreKey) => {
958 | try {
959 | let tempVal = NaN, tempMax = NaN, tempCrit = NaN;
960 | Object.keys(items[coreKey]).forEach((secondLevelKey) => {
961 | if (secondLevelKey.endsWith('_input')) {
962 | tempVal = cpuTempHelper.getTemp(parseFloat(items[coreKey][secondLevelKey]));
963 | } else if (secondLevelKey.endsWith('_max')) {
964 | tempMax = cpuTempHelper.getTemp(parseFloat(items[coreKey][secondLevelKey]));
965 | } else if (secondLevelKey.endsWith('_crit')) {
966 | tempCrit = cpuTempHelper.getTemp(parseFloat(items[coreKey][secondLevelKey]));
967 | }
968 | });
969 |
970 | if (!isNaN(tempVal)) {
971 | let tempStyle = '';
972 | if (!isNaN(tempMax) && tempVal >= tempMax) {
973 | tempStyle = 'color: #FFC300; font-weight: bold;';
974 | }
975 | if (!isNaN(tempCrit) && tempVal >= tempCrit) {
976 | tempStyle = 'color: red; font-weight: bold;';
977 | }
978 |
979 | let tempStr = '';
980 |
981 | // Enhanced parsing for AMD temperatures
982 | if (coreKey.startsWith('Tccd')) {
983 | let tempIndex = coreKey.match(/Tccd(\d+)/);
984 | if (tempIndex !== null && tempIndex.length > 1) {
985 | tempIndex = tempIndex[1];
986 | tempStr = `${cpuTempCaption} ${tempIndex}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()}`;
987 | } else {
988 | tempStr = `${cpuTempCaption}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()}`;
989 | }
990 | }
991 | // Handle CPU Core Temp (single overall temperature)
992 | else if (coreKey === 'CPU Core Temp') {
993 | tempStr = `${cpuTempCaption}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()}`;
994 | }
995 | // Enhanced parsing for Intel cores (P-Core, E-Core, regular Core)
996 | else {
997 | let tempIndex = coreKey.match(/(?:P\s+Core|E\s+Core|Core)\s*(\d+)/);
998 | if (tempIndex !== null && tempIndex.length > 1) {
999 | tempIndex = tempIndex[1];
1000 | let coreType = coreKey.startsWith('P Core') ? 'P Core' :
1001 | coreKey.startsWith('E Core') ? 'E Core' :
1002 | cpuTempCaption;
1003 | tempStr = `${coreType} ${tempIndex}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()}`;
1004 | } else {
1005 | // fallback for CPUs which do not have a core index
1006 | let coreType = coreKey.startsWith('P Core') ? 'P Core' :
1007 | coreKey.startsWith('E Core') ? 'E Core' :
1008 | cpuTempCaption;
1009 | tempStr = `${coreType}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()}`;
1010 | }
1011 | }
1012 |
1013 | cpuTemps.push(tempStr);
1014 | }
1015 | } catch (e) { /*_*/ }
1016 | });
1017 |
1018 | if(cpuTemps.length > 0) {
1019 | temps.push(cpuTemps);
1020 | }
1021 | });
1022 |
1023 | let result = '';
1024 | temps.forEach((cpuTemps, cpuIndex) => {
1025 | const strCoreTemps = cpuTemps.map((strTemp, index, arr) => {
1026 | return strTemp + (index + 1 < arr.length ? (itemsPerRow > 0 && (index + 1) % itemsPerRow === 0 ? '
' : ' | ') : '');
1027 | })
1028 | if(strCoreTemps.length > 0) {
1029 | result += (cpuCount > 1 ? `CPU ${cpuIndex+1}: ` : '') + strCoreTemps.join('') + (cpuIndex < cpuCount ? '
' : '');
1030 | }
1031 | });
1032 |
1033 | return '