Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de13e2329b | ||
|
|
6f78347a0b | ||
|
|
8d70194b8a | ||
|
|
2a122ef3eb | ||
|
|
0476410de5 | ||
|
|
972c9ae956 | ||
|
|
69e2d78b0a | ||
|
|
bdb3f9f3df | ||
|
|
c768eac6e3 | ||
|
|
7cb9aefced | ||
|
|
d99d08cf5f | ||
|
|
8a9b7568a7 | ||
|
|
f754331c3a | ||
|
|
e46734cd37 | ||
|
|
bc87579d77 | ||
|
|
e59d09a788 | ||
|
|
cee8d04e36 | ||
|
|
8ebc2b4753 | ||
|
|
795b0ebf39 | ||
|
|
43514fc949 | ||
|
|
c1c26aff56 |
5
.github/workflows/prepare-release-pr.yml
vendored
5
.github/workflows/prepare-release-pr.yml
vendored
@ -58,7 +58,6 @@ jobs:
|
|||||||
if [[ "${{ github.event_name }}" == "push" ]]; then
|
if [[ "${{ github.event_name }}" == "push" ]]; then
|
||||||
# Always recreate from main — avoids rebase conflicts after PR merge
|
# Always recreate from main — avoids rebase conflicts after PR merge
|
||||||
git checkout -b chore/version-bump origin/main
|
git checkout -b chore/version-bump origin/main
|
||||||
git push --force origin chore/version-bump
|
|
||||||
else
|
else
|
||||||
# Label change: check out the existing branch
|
# Label change: check out the existing branch
|
||||||
git fetch origin chore/version-bump
|
git fetch origin chore/version-bump
|
||||||
@ -133,8 +132,8 @@ jobs:
|
|||||||
echo "$MAIN_CHANGELOG"
|
echo "$MAIN_CHANGELOG"
|
||||||
} > debian/changelog
|
} > debian/changelog
|
||||||
git add debian/changelog
|
git add debian/changelog
|
||||||
git diff --cached --quiet || git commit -m "chore: update changelog to v${NEW_VERSION}"
|
git commit -m "chore: update changelog to v${NEW_VERSION}"
|
||||||
git push --force-with-lease origin chore/version-bump
|
git push --force origin chore/version-bump
|
||||||
|
|
||||||
- name: Build PR body
|
- name: Build PR body
|
||||||
env:
|
env:
|
||||||
|
|||||||
61
.github/workflows/weekly-patch-test.yml
vendored
61
.github/workflows/weekly-patch-test.yml
vendored
@ -36,11 +36,20 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
needs: discover
|
needs: discover
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: debian:trixie
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
mod: ${{ fromJSON(needs.discover.outputs.mods) }}
|
mod: ${{ fromJSON(needs.discover.outputs.mods) }}
|
||||||
steps:
|
steps:
|
||||||
|
# ── Base tools (mirrors validate-mods.yml) ──────────────────────────────
|
||||||
|
- name: Install base tools
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates git sudo wget xz-utils dpkg nodejs perl
|
||||||
|
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@ -48,22 +57,32 @@ jobs:
|
|||||||
# ── Build (mirrors test-release.yml) ───────────────────────────────────
|
# ── Build (mirrors test-release.yml) ───────────────────────────────────
|
||||||
- name: Install build deps
|
- name: Install build deps
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update -qq
|
apt-get update -qq
|
||||||
sudo apt-get install -y devscripts debhelper build-essential
|
apt-get install -y devscripts debhelper build-essential
|
||||||
|
|
||||||
|
- name: Compute build version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
BASE_VERSION=$(grep -m1 '(' debian/changelog | sed 's/.*(\(.*\)).*/\1/')
|
||||||
|
SHORT_SHA=$(git rev-parse --short HEAD)
|
||||||
|
BUILD_VERSION="${BASE_VERSION}+weekly.${SHORT_SHA}"
|
||||||
|
echo "build_version=$BUILD_VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Building version: $BUILD_VERSION"
|
||||||
|
|
||||||
- name: Inject version into Config.pm
|
- name: Inject version into Config.pm
|
||||||
run: |
|
run: |
|
||||||
VERSION=$(grep -m1 '(' debian/changelog | sed 's/.*(\(.*\)).*/\1/')
|
BUILD_VERSION="${{ steps.version.outputs.build_version }}"
|
||||||
SHORT_SHA=$(git rev-parse --short HEAD)
|
sed -i -E "s/^our \\\$VERSION\s*=.*/our \\\$VERSION = '${BUILD_VERSION}';/" \
|
||||||
export BUILD_VERSION="${VERSION}-test-${SHORT_SHA}"
|
src/modules/node_info/files/Config.pm
|
||||||
perl -i -pe 'BEGIN { $v = $ENV{BUILD_VERSION} }
|
|
||||||
s/(our \$VERSION\s*=\s*'"'"')[^'"'"']*'"'"';/$1'"'"'$v'"'"';/
|
- name: Set package version in changelog
|
||||||
' src/modules/node_info/files/Config.pm
|
run: |
|
||||||
|
BUILD_VERSION="${{ steps.version.outputs.build_version }}"
|
||||||
|
sed -i -E "0,/\(([^)]*)\)/s//(${BUILD_VERSION})/" debian/changelog
|
||||||
|
|
||||||
- name: Generate dynamic debian rules
|
- name: Generate dynamic debian rules
|
||||||
run: |
|
run: |
|
||||||
bash build/gen-rules.sh >> debian/rules
|
bash build/gen-rules.sh >> debian/rules
|
||||||
bash build/gen-rules.sh conffiles >> debian/pve-mod.conffiles
|
|
||||||
|
|
||||||
- name: Build package
|
- name: Build package
|
||||||
run: dpkg-buildpackage -us -uc -b
|
run: dpkg-buildpackage -us -uc -b
|
||||||
@ -72,13 +91,26 @@ jobs:
|
|||||||
- name: Fetch upstream Proxmox files
|
- name: Fetch upstream Proxmox files
|
||||||
run: bash build/test/fetch-proxmox-files.sh
|
run: bash build/test/fetch-proxmox-files.sh
|
||||||
|
|
||||||
|
- name: Capture upstream package versions
|
||||||
|
id: pve_versions
|
||||||
|
run: |
|
||||||
|
PVE_MANAGER_VERSION=$(dpkg-query -W -f='${Version}' pve-manager 2>/dev/null || echo "unknown")
|
||||||
|
PWT_VERSION=$(dpkg-query -W -f='${Version}' proxmox-widget-toolkit 2>/dev/null || echo "unknown")
|
||||||
|
echo "pve_manager=${PVE_MANAGER_VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "proxmox_widget_toolkit=${PWT_VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "pve-manager: ${PVE_MANAGER_VERSION}"
|
||||||
|
echo "proxmox-widget-toolkit: ${PWT_VERSION}"
|
||||||
|
|
||||||
- name: Install built package
|
- name: Install built package
|
||||||
run: sudo apt-get install -y ../pve-mod_*.deb
|
run: apt-get install -y ../pve-mod_*.deb
|
||||||
|
|
||||||
- name: Test mod
|
- name: Test mod
|
||||||
run: |
|
run: |
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
sudo bash build/test/test-patches.sh "${{ matrix.mod }}" 2>&1 | tee test-output.log
|
bash build/test/test-patches.sh "${{ matrix.mod }}" 2>&1 | tee test-output.log
|
||||||
|
|
||||||
|
- name: Validate source syntax
|
||||||
|
run: bash build/test/test-syntax.sh "${{ matrix.mod }}"
|
||||||
|
|
||||||
- name: Report failure as issue
|
- name: Report failure as issue
|
||||||
if: failure()
|
if: failure()
|
||||||
@ -90,6 +122,10 @@ jobs:
|
|||||||
{
|
{
|
||||||
echo "The weekly patch test failed for mod \`${MOD}\` against the latest Proxmox VE files."
|
echo "The weekly patch test failed for mod \`${MOD}\` against the latest Proxmox VE files."
|
||||||
echo ""
|
echo ""
|
||||||
|
echo "**Proxmox versions tested:**"
|
||||||
|
echo "- pve-manager: \`${{ steps.pve_versions.outputs.pve_manager }}\`"
|
||||||
|
echo "- proxmox-widget-toolkit: \`${{ steps.pve_versions.outputs.proxmox_widget_toolkit }}\`"
|
||||||
|
echo ""
|
||||||
echo "Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
echo "Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||||
echo ""
|
echo ""
|
||||||
echo "<details><summary>Test log</summary>"
|
echo "<details><summary>Test log</summary>"
|
||||||
@ -109,8 +145,9 @@ jobs:
|
|||||||
--jq ".[] | select(.title==\"$TITLE\") | .number" | head -n1)
|
--jq ".[] | select(.title==\"$TITLE\") | .number" | head -n1)
|
||||||
|
|
||||||
if [[ -n "$EXISTING" ]]; then
|
if [[ -n "$EXISTING" ]]; then
|
||||||
|
gh issue edit "$EXISTING" --body-file issue-body.md
|
||||||
gh issue comment "$EXISTING" --body-file issue-body.md
|
gh issue comment "$EXISTING" --body-file issue-body.md
|
||||||
echo "Commented on existing issue #$EXISTING"
|
echo "Updated and commented on existing issue #$EXISTING"
|
||||||
else
|
else
|
||||||
gh issue create --title "$TITLE" --label patch-break --body-file issue-body.md
|
gh issue create --title "$TITLE" --label patch-break --body-file issue-body.md
|
||||||
echo "Opened new issue for mod $MOD"
|
echo "Opened new issue for mod $MOD"
|
||||||
|
|||||||
@ -121,7 +121,7 @@ test_one_mod() {
|
|||||||
if [[ $ok -eq 0 ]]; then
|
if [[ $ok -eq 0 ]]; then
|
||||||
info "Reverting patches for '$mod'..."
|
info "Reverting patches for '$mod'..."
|
||||||
local revert_log
|
local revert_log
|
||||||
revert_log="$("$REVERT" 2>&1)"
|
revert_log="$("$REVERT" "$mod" 2>&1)"
|
||||||
echo "$revert_log"
|
echo "$revert_log"
|
||||||
if echo "$revert_log" | grep -qE "could not be reverted cleanly|reported an error"; then
|
if echo "$revert_log" | grep -qE "could not be reverted cleanly|reported an error"; then
|
||||||
warn "revert-patches.sh reported an unclean revert for mod '$mod'"
|
warn "revert-patches.sh reported an unclean revert for mod '$mod'"
|
||||||
|
|||||||
17
debian/changelog
vendored
17
debian/changelog
vendored
@ -1,3 +1,20 @@
|
|||||||
|
pve-mod (1.0.7) stable; urgency=low
|
||||||
|
|
||||||
|
* 8d70194 fix(nag_screen): Patch typo (#318)
|
||||||
|
* 2a122ef feat(node_info) Add configurable minimum-temperature ignore threshold (#316)
|
||||||
|
* 0476410 fix(node_info): re-show sensor widgets when data becomes valid again after being hidden (#312)
|
||||||
|
* 972c9ae fix(node_info): reap zombie pve_mod_worker and orphaned collector processes (#307)
|
||||||
|
* 69e2d78 feat(node_info): UPS widget: redesign status panel with battery-charge progress bar (#310)
|
||||||
|
* bdb3f9f fix(node_info): normalize UPS data — derive ups.realpower fallback and clean invalid battery.mfr.date (#309)
|
||||||
|
* c768eac fix(nag_screen): Fix nag screen patch webui line offsets (#315)
|
||||||
|
* 8a9b756 fix(node_info): hide GPU Details and GPU Fans when no GPU is present (#311)
|
||||||
|
* f754331 fix(node_info): fix stale worker lock detection for zombie processes (#306)
|
||||||
|
* e46734c fix(node_info): return average and core temps for intel (#302)
|
||||||
|
* cee8d04 feat/fix(node_info): expose temp_unit in graphics API response & fix temperature conversion (#295)
|
||||||
|
* 8ebc2b4 feat(node_info): add GPU utilization display with color-coded thresholds for NVIDIA GPUs (#296)
|
||||||
|
|
||||||
|
-- github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Sat, 22 Aug 2026 23:24:00 +0000
|
||||||
|
|
||||||
pve-mod (1.0.6) stable; urgency=low
|
pve-mod (1.0.6) stable; urgency=low
|
||||||
|
|
||||||
* c21b973 remove buildtest file (#288)
|
* c21b973 remove buildtest file (#288)
|
||||||
|
|||||||
1
debian/pve-mod.triggers
vendored
1
debian/pve-mod.triggers
vendored
@ -1,3 +1,4 @@
|
|||||||
interest-noawait /usr/share/pve-manager/js
|
interest-noawait /usr/share/pve-manager/js
|
||||||
interest-noawait /usr/share/perl5/PVE
|
interest-noawait /usr/share/perl5/PVE
|
||||||
interest-noawait /usr/share/pve-yew-mobile-gui
|
interest-noawait /usr/share/pve-yew-mobile-gui
|
||||||
|
interest-noawait /usr/share/javascript/proxmox-widget-toolkit
|
||||||
|
|||||||
85
readme.md
85
readme.md
@ -3,11 +3,80 @@ A small collection of scripts and mods for Proxmox Virtual Environment (PVE)
|
|||||||
|
|
||||||
If you find this helpful, a small donation is appreciated, [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=K8XPMSEBERH3W).
|
If you find this helpful, a small donation is appreciated, [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=K8XPMSEBERH3W).
|
||||||
|
|
||||||
## Node sensor readings view
|
| Version | Description | Status |
|
||||||
|
|---------|-------------|--------|
|
||||||
|
| **v2** (current) | Debian package (`pve-mod`) with interactive `pve-mod-configure` wizard | Recommended |
|
||||||
|
| v1 (legacy) | Standalone bash scripts, manual install | Archived — see [Legacy section](#legacy-v1-shell-scripts) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version 2 (beta) — Debian package
|
||||||
|
|
||||||
|
Compatibility: PVE 9.0+
|
||||||
|
|
||||||
|
TBD PICTURE
|
||||||
|
|
||||||
|
`pve-mod` is a Debian package that installs UI patches and an interactive configuration wizard.
|
||||||
|
|
||||||
|
### Mods
|
||||||
|
|
||||||
|
| Mod | Description | Dependencies |
|
||||||
|
|--------|-------------|--------------|
|
||||||
|
| [`node_info`](src/modules/node_info/readme.md) | Displays sensor readings in the node summary panel: CPU, NVMe/HDD/SSD temperatures (°C/°F), fan speeds, RAM temperatures, GPU stats (Intel/NVIDIA), UPS status, and system/motherboard info. <br> Can optionally run as background sensor daemon | - General sensors: `lm-sensors`<br>- HDD/SSD: Kernal module `drivetemp`<br>- UPS: `upsc`<br>- GPU: INTEL `intel-gpu-tools` and/or NVIDIA `nvidia-driver-* `
|
||||||
|
| [`nag_screen`](src/modules/nag_screen/readme.md) | Removes the subscription nag screen from the PVE web UI. | - |
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
1. The `pve-mod` package installs the mods installation, patch files under `/usr/lib/pve-mod/`.
|
||||||
|
2. The mods main configuration file and mods can be found under `/etc/pve-mod/`.
|
||||||
|
2. Running `pve-mod-configure` prompts for which modules to enable and configuration of mods.
|
||||||
|
3. The wizard applies the selected patches to the PVE system files and restarts `pveproxy`.
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
Commands and mod configuration must be run as `root`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sL https://raw.githubusercontent.com/Meliox/PVE-mods/refs/heads/main/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Install respective mod dependencies - see Mod table.
|
||||||
|
|
||||||
|
Run `pve-mod-configure` to install/uninstall and configure mods.
|
||||||
|
|
||||||
|
#### Manual configuration changes
|
||||||
|
Configuration files are located in `/etc/pve-mod/`. After any manual edits, restart `pveproxy` to apply the changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl restart pveproxy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Uninstall
|
||||||
|
|
||||||
|
Commands and mod configuration must be run as `root`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
apt-get remove pve-mod
|
||||||
|
```
|
||||||
|
And remove dependencies needed by respective mods.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
Multiple node support requires application to be installed on all nodes with identical configuration. (untested)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Legacy / v1 (shell scripts)
|
||||||
|
|
||||||
|
> **These scripts are archived.** For new installations, use the [v2 Debian package](#version-2--debian-package) above.
|
||||||
|
|
||||||
|
### Node sensor readings view
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
Compatibility:
|
Compatibility:
|
||||||
- 9.0-9.2. Newer versions may often work
|
- 9.0-9.2. Newer versions may often work
|
||||||
- Older version (7.x-8.x), use git version from Apr 6th 2025
|
- Older version (7.x-8.x), use git version from Apr 6th 2025
|
||||||

|
|
||||||
|
|
||||||
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.
|
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.
|
||||||
The following readings are possible:
|
The following readings are possible:
|
||||||
@ -15,7 +84,7 @@ The following readings are possible:
|
|||||||
- UPS information via Network Monitoring Tool
|
- UPS information via Network Monitoring Tool
|
||||||
- Motherboard information or system information via dmidecode
|
- Motherboard information or system information via dmidecode
|
||||||
|
|
||||||
### How it works
|
#### How it works
|
||||||
The modification involves the following steps:
|
The modification involves the following steps:
|
||||||
1. Backup original files in the home directory/backup
|
1. Backup original files in the home directory/backup
|
||||||
- `/usr/share/pve-manager/js/pvemanagerlib.js`
|
- `/usr/share/pve-manager/js/pvemanagerlib.js`
|
||||||
@ -38,7 +107,7 @@ Notes:
|
|||||||
- UPS support in multi-node setups require identical login credentials across nodes. This has not been fully tested.
|
- UPS support in multi-node setups require identical login credentials across nodes. This has not been fully tested.
|
||||||
- Proxmox upgrades may overwrite modified files; reinstallation of this mod could be required.
|
- Proxmox upgrades may overwrite modified files; reinstallation of this mod could be required.
|
||||||
|
|
||||||
### Install
|
#### Install
|
||||||
Instructions be performed as 'root', as normal users do not have access to the files.
|
Instructions be performed as 'root', as normal users do not have access to the files.
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -51,7 +120,7 @@ bash pve-mod-gui-sensors.sh install
|
|||||||
```
|
```
|
||||||
Additionally, adjustments are available in the first part of the script, where paths can be edited, cpucore offset and display information.
|
Additionally, adjustments are available in the first part of the script, where paths can be edited, cpucore offset and display information.
|
||||||
|
|
||||||
## Nag screen deactivation
|
### Nag screen deactivation
|
||||||
(Tested compatibility: 7.x - 8.3.5)
|
(Tested compatibility: 7.x - 8.3.5)
|
||||||
This bash script installs a modification to the Proxmox Virtual Environment (PVE) web user interface (UI) which deactivates the subscription nag screen.
|
This bash script installs a modification to the Proxmox Virtual Environment (PVE) web user interface (UI) which deactivates the subscription nag screen.
|
||||||
|
|
||||||
@ -65,21 +134,21 @@ The script provides three options:
|
|||||||
| `install` | Installs the modification by applying the necessary changes. |
|
| `install` | Installs the modification by applying the necessary changes. |
|
||||||
| `uninstall` | Removes the modification by restoring the original files from backups. |
|
| `uninstall` | Removes the modification by restoring the original files from backups. |
|
||||||
|
|
||||||
### Install
|
#### Install
|
||||||
Instructions be performed as 'root', as normal users do not have access to the files.
|
Instructions be performed as 'root', as normal users do not have access to the files.
|
||||||
```
|
```
|
||||||
wget https://raw.githubusercontent.com/Meliox/PVE-mods/refs/heads/main/legacy-scripts/pve-mod-nag-screen.sh
|
wget https://raw.githubusercontent.com/Meliox/PVE-mods/refs/heads/main/legacy-scripts/pve-mod-nag-screen.sh
|
||||||
bash pve-mod-nag-screen.sh install
|
bash pve-mod-nag-screen.sh install
|
||||||
```
|
```
|
||||||
|
|
||||||
## Script to update all containers
|
### Script to update all containers
|
||||||
(Tested compatibility: 7.x - 8.3.5)
|
(Tested compatibility: 7.x - 8.3.5)
|
||||||
|
|
||||||
This script updates all running Proxmox containers, skipping specified excluded containers, and generates a separate log file for each container.
|
This script updates all running Proxmox containers, skipping specified excluded containers, and generates a separate log file for each container.
|
||||||
The script first updates the Proxmox host system, then iterates through each container, updates the container, and reboots it if necessary.
|
The script first updates the Proxmox host system, then iterates through each container, updates the container, and reboots it if necessary.
|
||||||
Each container's log file is stored in $log_path and the main script log file is named container-upgrade-main.log.
|
Each container's log file is stored in $log_path and the main script log file is named container-upgrade-main.log.
|
||||||
|
|
||||||
### Install
|
#### Install
|
||||||
```
|
```
|
||||||
wget https://raw.githubusercontent.com/Meliox/PVE-mods/refs/heads/main/legacy-scripts/updateallcontainers.sh
|
wget https://raw.githubusercontent.com/Meliox/PVE-mods/refs/heads/main/legacy-scripts/updateallcontainers.sh
|
||||||
```
|
```
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
--- a/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js
|
--- /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.orig 2026-08-23 01:17:11.563403975 +0200
|
||||||
+++ b/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js
|
+++ /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js 2026-08-23 01:19:00.972084488 +0200
|
||||||
@@ -599,37 +599,8 @@
|
@@ -612,37 +612,8 @@
|
||||||
Ext.Msg.alert(gettext('Error'), res.htmlStatus || res.result.message),
|
),
|
||||||
|
|
||||||
checked_command: function (orig_cmd) {
|
checked_command: function (orig_cmd) {
|
||||||
- Proxmox.Utils.API2Request({
|
- Proxmox.Utils.API2Request({
|
||||||
|
|||||||
@ -97,7 +97,9 @@ sub _get_temperature_sensors {
|
|||||||
fans => $config{lm_sensors}{enable_fan_speed} ? \1 : \0,
|
fans => $config{lm_sensors}{enable_fan_speed} ? \1 : \0,
|
||||||
hdd => $config{lm_sensors}{enable_hdd_temp} ? \1 : \0,
|
hdd => $config{lm_sensors}{enable_hdd_temp} ? \1 : \0,
|
||||||
other => $config{lm_sensors}{enable_other_temp} ? \1 : \0,
|
other => $config{lm_sensors}{enable_other_temp} ? \1 : \0,
|
||||||
temp_unit => $config{lm_sensors}{temp_unit},
|
temp_unit => $config{system_info}{temp_unit},
|
||||||
|
cpu_temp_target => $config{lm_sensors}{cpu_temp_target},
|
||||||
|
ignore_temp_below => $config{system_info}{ignore_temp_below} + 0,
|
||||||
data => { 'PVE MOD lm-sensors Enhanced' => $sensors_json },
|
data => { 'PVE MOD lm-sensors Enhanced' => $sensors_json },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -112,6 +112,8 @@ sub _parse_upsc_output {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_normalize_ups_data($ups_data);
|
||||||
};
|
};
|
||||||
if ($@) {
|
if ($@) {
|
||||||
debug(__LINE__, "Error parsing upsc output: $@");
|
debug(__LINE__, "Error parsing upsc output: $@");
|
||||||
@ -122,4 +124,28 @@ sub _parse_upsc_output {
|
|||||||
return $ups_data;
|
return $ups_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# UPS — data normalization
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
sub _normalize_ups_data {
|
||||||
|
my ($ups_data) = @_;
|
||||||
|
|
||||||
|
# Some devices report a placeholder (e.g. "OPEN") instead of a real date
|
||||||
|
if (defined $ups_data->{'battery.mfr.date'} && $ups_data->{'battery.mfr.date'} !~ m{^\d{4}[/-]\d{1,2}[/-]\d{1,2}$}) {
|
||||||
|
delete $ups_data->{'battery.mfr.date'};
|
||||||
|
}
|
||||||
|
|
||||||
|
# Derive real power draw for devices that don't report ups.realpower directly
|
||||||
|
if (!defined $ups_data->{'ups.realpower'}) {
|
||||||
|
if (defined $ups_data->{'ups.load'} && defined $ups_data->{'ups.realpower.nominal'}) {
|
||||||
|
$ups_data->{'ups.realpower'} = int((($ups_data->{'ups.load'} / 100) * $ups_data->{'ups.realpower.nominal'}) + 0.5);
|
||||||
|
} elsif (defined $ups_data->{'output.current'} && defined $ups_data->{'output.voltage'}) {
|
||||||
|
$ups_data->{'ups.realpower'} = int(($ups_data->{'output.current'} * $ups_data->{'output.voltage'}) + 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
1;
|
1;
|
||||||
|
|||||||
@ -72,7 +72,6 @@ our %config = (
|
|||||||
enable_other_temp => 0,
|
enable_other_temp => 0,
|
||||||
enable_fan_speed => 0,
|
enable_fan_speed => 0,
|
||||||
display_zero_speed_fans => 0,
|
display_zero_speed_fans => 0,
|
||||||
temp_unit => 'C',
|
|
||||||
},
|
},
|
||||||
ups => {
|
ups => {
|
||||||
enabled => 0,
|
enabled => 0,
|
||||||
@ -81,6 +80,8 @@ our %config = (
|
|||||||
system_info => {
|
system_info => {
|
||||||
enabled => 0,
|
enabled => 0,
|
||||||
type => 1, # 1 = System (dmidecode -t 1), 2 = Baseboard/Motherboard (dmidecode -t 2)
|
type => 1, # 1 = System (dmidecode -t 1), 2 = Baseboard/Motherboard (dmidecode -t 2)
|
||||||
|
temp_unit => 'C',
|
||||||
|
ignore_temp_below => 5,
|
||||||
},
|
},
|
||||||
paths => {
|
paths => {
|
||||||
working_dir => '/run/pveproxy/pve-mod',
|
working_dir => '/run/pveproxy/pve-mod',
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use strict;
|
|||||||
use warnings;
|
use warnings;
|
||||||
use Exporter 'import';
|
use Exporter 'import';
|
||||||
|
|
||||||
use POSIX qw(WNOHANG);
|
use POSIX qw(WNOHANG setsid);
|
||||||
use File::Path qw(remove_tree);
|
use File::Path qw(remove_tree);
|
||||||
|
|
||||||
use PVE::PVEMod::Config qw(
|
use PVE::PVEMod::Config qw(
|
||||||
@ -13,7 +13,7 @@ use PVE::PVEMod::Config qw(
|
|||||||
$pve_mod_worker_lock $startup_lock
|
$pve_mod_worker_lock $startup_lock
|
||||||
);
|
);
|
||||||
use PVE::PVEMod::Utils qw(
|
use PVE::PVEMod::Utils qw(
|
||||||
debug is_process_alive read_lock_pid
|
debug is_process_alive get_process_ppid read_lock_pid
|
||||||
acquire_exclusive_lock ensure_pve_mod_directory_exists
|
acquire_exclusive_lock ensure_pve_mod_directory_exists
|
||||||
check_executable startup_message
|
check_executable startup_message
|
||||||
);
|
);
|
||||||
@ -112,10 +112,37 @@ sub notify_pve_mod_worker {
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
sub _worker_lock_file_exists {
|
sub _worker_lock_file_exists {
|
||||||
return -f $pve_mod_worker_lock;
|
return 0 unless -f $pve_mod_worker_lock;
|
||||||
|
|
||||||
|
my $pid = read_lock_pid($pve_mod_worker_lock);
|
||||||
|
if (!defined $pid || $pid !~ /^(\d+)$/) {
|
||||||
|
debug(__LINE__, "Worker lock is invalid (PID: " . ($pid // 'undefined') . "), removing");
|
||||||
|
unlink($pve_mod_worker_lock);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
my $worker_pid = $1;
|
||||||
|
|
||||||
|
if (is_process_alive($worker_pid)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(__LINE__, "Stale or zombie worker lock found for PID $worker_pid, reaping if needed");
|
||||||
|
my $reaped_pid = waitpid($worker_pid, WNOHANG);
|
||||||
|
if ($reaped_pid == $worker_pid) {
|
||||||
|
debug(__LINE__, "Reaped stale worker PID $worker_pid");
|
||||||
|
} elsif ($reaped_pid == -1 && $!{ECHILD}) {
|
||||||
|
# Expected once the worker is double-forked: we're not its parent, init is.
|
||||||
|
debug(__LINE__, "PID $worker_pid is not a child of this process; treating lock as stale");
|
||||||
|
} else {
|
||||||
|
debug(__LINE__, "waitpid on PID $worker_pid returned $reaped_pid: $!");
|
||||||
|
}
|
||||||
|
|
||||||
|
unlink($pve_mod_worker_lock);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Forks the worker process and records its PID in the lock file.
|
# Double-forks so the worker is reparented to init (PID 1) instead of the
|
||||||
|
# caller; init reaps it on exit, so it never lingers as a <defunct> zombie.
|
||||||
sub _pve_mod_worker {
|
sub _pve_mod_worker {
|
||||||
debug(__LINE__, "_pve_mod_worker called");
|
debug(__LINE__, "_pve_mod_worker called");
|
||||||
|
|
||||||
@ -125,34 +152,57 @@ sub _pve_mod_worker {
|
|||||||
print $pve_mod_worker_fh "$$\n";
|
print $pve_mod_worker_fh "$$\n";
|
||||||
close($pve_mod_worker_fh);
|
close($pve_mod_worker_fh);
|
||||||
|
|
||||||
debug(__LINE__, "Forking new pve_mod_worker process");
|
debug(__LINE__, "Forking intermediate process for pve_mod_worker");
|
||||||
my $pve_mod_worker_pid = fork();
|
my $intermediate_pid = fork();
|
||||||
|
|
||||||
unless (defined $pve_mod_worker_pid) {
|
unless (defined $intermediate_pid) {
|
||||||
debug(__LINE__, "Failed to fork pve_mod_worker process: $!");
|
debug(__LINE__, "Failed to fork intermediate pve_mod_worker process: $!");
|
||||||
|
unlink($pve_mod_worker_lock);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($pve_mod_worker_pid == 0) {
|
if ($intermediate_pid == 0) {
|
||||||
# Child
|
# Intermediate child: detach into its own session, fork the real
|
||||||
$0 = "pve_mod_worker_controller";
|
# worker, then exit immediately so the worker is reparented to init.
|
||||||
debug(__LINE__, "Child process forked, calling _pve_mod_keep_alive");
|
setsid();
|
||||||
_pve_mod_keep_alive();
|
|
||||||
exit(0);
|
my $worker_pid = fork();
|
||||||
|
unless (defined $worker_pid) {
|
||||||
|
debug(__LINE__, "Failed to fork pve_mod_worker process: $!");
|
||||||
|
unlink($pve_mod_worker_lock);
|
||||||
|
POSIX::_exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($worker_pid == 0) {
|
||||||
|
# Grandchild — the actual worker
|
||||||
|
$0 = "pve_mod_worker_controller";
|
||||||
|
if (open my $fh, '>', $pve_mod_worker_lock) {
|
||||||
|
print $fh "$$\n";
|
||||||
|
close $fh;
|
||||||
|
debug(__LINE__, "Wrote pve_mod_worker PID to lock file: $pve_mod_worker_lock");
|
||||||
|
} else {
|
||||||
|
debug(__LINE__, "Failed to write pve_mod_worker lock file: $!");
|
||||||
|
}
|
||||||
|
debug(__LINE__, "Worker process forked, calling _pve_mod_keep_alive");
|
||||||
|
_pve_mod_keep_alive();
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(__LINE__, "Intermediate process exiting, worker PID $worker_pid reparented to init");
|
||||||
|
# _exit (not exit) so we skip END blocks/global destruction — this
|
||||||
|
# process only ever existed to perform the double fork.
|
||||||
|
POSIX::_exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
# Parent — update lock file with real child PID
|
# Parent — reap the short-lived intermediate process immediately, so it
|
||||||
debug(__LINE__, "Forked pve_mod_worker process with PID $pve_mod_worker_pid");
|
# never has a chance to become a zombie under us either.
|
||||||
if (open my $fh, '>', $pve_mod_worker_lock) {
|
waitpid($intermediate_pid, 0);
|
||||||
print $fh "$pve_mod_worker_pid\n";
|
my $status = $? >> 8;
|
||||||
close $fh;
|
if ($status == 0) {
|
||||||
debug(__LINE__, "Wrote pve_mod_worker PID to lock file: $pve_mod_worker_lock");
|
debug(__LINE__, "pve_mod_worker process started successfully");
|
||||||
} else {
|
} else {
|
||||||
debug(__LINE__, "Failed to write pve_mod_worker lock file: $!");
|
debug(__LINE__, "Intermediate pve_mod_worker process exited with status $status");
|
||||||
kill('TERM', $pve_mod_worker_pid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debug(__LINE__, "pve_mod_worker process started successfully");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@ -199,6 +249,8 @@ sub _pve_mod_keep_alive {
|
|||||||
exit(0);
|
exit(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_reap_orphaned_collectors();
|
||||||
|
|
||||||
debug(__LINE__, "Worker starting all collectors");
|
debug(__LINE__, "Worker starting all collectors");
|
||||||
_initialise_sensors_collector();
|
_initialise_sensors_collector();
|
||||||
_initialise_graphics_collectors();
|
_initialise_graphics_collectors();
|
||||||
@ -451,6 +503,74 @@ sub _stop_child_collectors {
|
|||||||
debug(__LINE__, "Cleanup complete");
|
debug(__LINE__, "Cleanup complete");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Finds and terminates collector processes left behind by a previous worker
|
||||||
|
# that never got to run _stop_child_collectors() (e.g. killed with SIGKILL).
|
||||||
|
# Only targets processes reparented to init, never a running worker's own.
|
||||||
|
sub _reap_orphaned_collectors {
|
||||||
|
debug(__LINE__, "Scanning for orphaned collector processes from a previous worker");
|
||||||
|
|
||||||
|
my $dh;
|
||||||
|
unless (opendir($dh, '/proc')) {
|
||||||
|
debug(__LINE__, "Failed to open /proc: $!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
my @orphans;
|
||||||
|
while (my $entry = readdir($dh)) {
|
||||||
|
next unless $entry =~ /^(\d+)$/;
|
||||||
|
my $pid = $1;
|
||||||
|
next if $pid == $$;
|
||||||
|
|
||||||
|
next unless open(my $fh, '<', "/proc/$pid/cmdline");
|
||||||
|
my $cmdline = <$fh>;
|
||||||
|
close($fh);
|
||||||
|
next unless defined $cmdline;
|
||||||
|
|
||||||
|
my ($name) = split /\0/, $cmdline;
|
||||||
|
next unless defined $name && $name =~ /^collector-/;
|
||||||
|
|
||||||
|
my $ppid = get_process_ppid($pid);
|
||||||
|
next unless defined $ppid && $ppid == 1;
|
||||||
|
|
||||||
|
push @orphans, $pid;
|
||||||
|
}
|
||||||
|
closedir($dh);
|
||||||
|
|
||||||
|
unless (@orphans) {
|
||||||
|
debug(__LINE__, "No orphaned collectors found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(__LINE__, "Found " . scalar(@orphans) . " orphaned collector(s), terminating");
|
||||||
|
foreach my $pid (@orphans) {
|
||||||
|
kill('TERM', $pid);
|
||||||
|
debug(__LINE__, "Sent SIGTERM to orphaned collector PID $pid");
|
||||||
|
}
|
||||||
|
|
||||||
|
# Orphans are already zombies as soon as init reaps them, and kill(0,...)
|
||||||
|
# keeps reporting a zombie's PID as present - use is_process_alive() so we
|
||||||
|
# don't spin the full timeout (or send a pointless KILL) on a dead PID.
|
||||||
|
my $timeout = 2;
|
||||||
|
my $start = time();
|
||||||
|
while (time() - $start < $timeout) {
|
||||||
|
my $any_alive = 0;
|
||||||
|
foreach my $pid (@orphans) {
|
||||||
|
if (is_process_alive($pid)) { $any_alive = 1; last; }
|
||||||
|
}
|
||||||
|
last unless $any_alive;
|
||||||
|
select(undef, undef, undef, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach my $pid (@orphans) {
|
||||||
|
if (is_process_alive($pid)) {
|
||||||
|
debug(__LINE__, "Force killing orphaned collector process $pid");
|
||||||
|
kill('KILL', $pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(__LINE__, "Orphan cleanup complete");
|
||||||
|
}
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# END block — only the worker process performs cleanup
|
# END block — only the worker process performs cleanup
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@ -68,9 +68,11 @@ sub _merge_graphics_files {
|
|||||||
|
|
||||||
my $merged = {
|
my $merged = {
|
||||||
Graphics => {
|
Graphics => {
|
||||||
Intel => {},
|
Intel => {},
|
||||||
NVIDIA => {},
|
NVIDIA => {},
|
||||||
AMD => {},
|
AMD => {},
|
||||||
|
temp_unit => $config{system_info}{temp_unit},
|
||||||
|
ignore_temp_below => $config{system_info}{ignore_temp_below} + 0,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -134,7 +136,7 @@ sub _load_graphics_data {
|
|||||||
\@filepaths,
|
\@filepaths,
|
||||||
$graphics_cache,
|
$graphics_cache,
|
||||||
\&_merge_graphics_files,
|
\&_merge_graphics_files,
|
||||||
{ Graphics => { Intel => {}, NVIDIA => {}, AMD => {} } }
|
{ Graphics => { Intel => {}, NVIDIA => {}, AMD => {}, temp_unit => $config{system_info}{temp_unit}, ignore_temp_below => $config{system_info}{ignore_temp_below} + 0 } }
|
||||||
);
|
);
|
||||||
|
|
||||||
my $intel_count = scalar(keys %{$data->{Graphics}{Intel} // {}});
|
my $intel_count = scalar(keys %{$data->{Graphics}{Intel} // {}});
|
||||||
|
|||||||
@ -223,15 +223,16 @@ Ext.define('PVE.node.StatusView', {
|
|||||||
} catch(e) {
|
} catch(e) {
|
||||||
objValue = {};
|
objValue = {};
|
||||||
}
|
}
|
||||||
|
this.show();
|
||||||
// sensors configuration
|
// sensors configuration
|
||||||
const cpuTempHelper = Ext.create('PVE.mod.TempHelper', {srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: objValue.temp_unit === 'F' ? PVE.mod.TempHelper.FAHRENHEIT : PVE.mod.TempHelper.CELSIUS});
|
const cpuTempHelper = Ext.create('PVE.mod.TempHelper', {srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: value.temp_unit === 'F' ? PVE.mod.TempHelper.FAHRENHEIT : PVE.mod.TempHelper.CELSIUS});
|
||||||
|
const cpuIgnoreThreshold = cpuTempHelper.getTemp(parseFloat(value.ignore_temp_below));
|
||||||
const cpuKeysI = Object.keys(objValue).filter(item => String(item).startsWith('coretemp-isa-')).sort();
|
const cpuKeysI = Object.keys(objValue).filter(item => String(item).startsWith('coretemp-isa-')).sort();
|
||||||
const cpuKeysA = Object.keys(objValue).filter(item => String(item).startsWith('k10temp-pci-')).sort();
|
const cpuKeysA = Object.keys(objValue).filter(item => String(item).startsWith('k10temp-pci-')).sort();
|
||||||
const cpuKeysRpi = Object.keys(objValue).filter(item => String(item).startsWith('cpu_thermal-virtual-')).sort();
|
const cpuKeysRpi = Object.keys(objValue).filter(item => String(item).startsWith('cpu_thermal-virtual-')).sort();
|
||||||
const bINTEL = cpuKeysI.length > 0 ? true : false;
|
const bINTEL = cpuKeysI.length > 0 ? true : false;
|
||||||
const INTELPackagePrefix = 'Core' == 'Core' ? 'Core ' : 'Package id';
|
const INTELPackagePrefix = value.cpu_temp_target == 'Core' ? 'Core ' : 'Package id';
|
||||||
const INTELPackageCaption = 'Core' == 'Core' ? 'Core' : 'Package';
|
const INTELPackageCaption = value.cpu_temp_target == 'Core' ? 'Core' : 'Package';
|
||||||
let AMDPackagePrefix = 'Tccd';
|
let AMDPackagePrefix = 'Tccd';
|
||||||
let AMDPackageCaption = 'CCD';
|
let AMDPackageCaption = 'CCD';
|
||||||
|
|
||||||
@ -305,7 +306,7 @@ Ext.define('PVE.node.StatusView', {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isNaN(tempVal)) {
|
if (!isNaN(tempVal) && tempVal >= cpuIgnoreThreshold) {
|
||||||
let tempStyle = '';
|
let tempStyle = '';
|
||||||
if (!isNaN(tempMax) && tempVal >= tempMax) {
|
if (!isNaN(tempMax) && tempVal >= tempMax) {
|
||||||
tempStyle = 'color: #FFC300; font-weight: bold;';
|
tempStyle = 'color: #FFC300; font-weight: bold;';
|
||||||
@ -392,9 +393,16 @@ Ext.define('PVE.node.StatusView', {
|
|||||||
this.hide();
|
this.hide();
|
||||||
return '';
|
return '';
|
||||||
} else if (!gpuStats || !gpuStats.Graphics) {
|
} else if (!gpuStats || !gpuStats.Graphics) {
|
||||||
|
this.hide();
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create temperature helper for GPU temperature conversion
|
||||||
|
const gpuTempHelper = Ext.create('PVE.mod.TempHelper', {
|
||||||
|
srcUnit: PVE.mod.TempHelper.CELSIUS,
|
||||||
|
dstUnit: gpuStats.temp_unit === 'F' ? PVE.mod.TempHelper.FAHRENHEIT : PVE.mod.TempHelper.CELSIUS
|
||||||
|
});
|
||||||
|
|
||||||
let html = '<table style="width: 100%; border-collapse: collapse; table-layout: fixed;">';
|
let html = '<table style="width: 100%; border-collapse: collapse; table-layout: fixed;">';
|
||||||
|
|
||||||
// Intel GPUs - Secondary details
|
// Intel GPUs - Secondary details
|
||||||
@ -445,6 +453,15 @@ Ext.define('PVE.node.StatusView', {
|
|||||||
|
|
||||||
let details = [];
|
let details = [];
|
||||||
|
|
||||||
|
// GPU Utilization
|
||||||
|
if (stats.utilization && stats.utilization.gpu) {
|
||||||
|
const gpuUsage = parseInt(stats.utilization.gpu);
|
||||||
|
let gpuStyle = '';
|
||||||
|
if (gpuUsage >= 90) gpuStyle = 'color: #d9534f; font-weight: bold;';
|
||||||
|
else if (gpuUsage >= 70) gpuStyle = 'color: #f0ad4e; font-weight: bold;';
|
||||||
|
details.push(`<span style="${gpuStyle}">GPU: ${stats.utilization.gpu}%</span>`);
|
||||||
|
}
|
||||||
|
|
||||||
// Memory Utilization
|
// Memory Utilization
|
||||||
if (stats.utilization && stats.utilization.memory) {
|
if (stats.utilization && stats.utilization.memory) {
|
||||||
const memUsage = parseInt(stats.utilization.memory);
|
const memUsage = parseInt(stats.utilization.memory);
|
||||||
@ -467,13 +484,21 @@ Ext.define('PVE.node.StatusView', {
|
|||||||
|
|
||||||
// Temperature
|
// Temperature
|
||||||
if (stats.temperature) {
|
if (stats.temperature) {
|
||||||
let tempStyle = '';
|
const gpuTemp = gpuTempHelper.getTemp(parseFloat(stats.temperature.gpu));
|
||||||
if (stats.temperature.gpu >= 80) {
|
const gpuIgnoreThreshold = gpuTempHelper.getTemp(parseFloat(gpuStats.ignore_temp_below));
|
||||||
tempStyle = 'color: red; font-weight: bold;';
|
if (gpuTemp >= gpuIgnoreThreshold) {
|
||||||
} else if (stats.temperature.gpu >= 70) {
|
const tempUnit = gpuTempHelper.getUnit();
|
||||||
tempStyle = 'color: #FFC300; font-weight: bold;';
|
// Convert thresholds to target unit for comparison
|
||||||
|
const tempHigh = gpuTempHelper.getTemp(80);
|
||||||
|
const tempWarn = gpuTempHelper.getTemp(70);
|
||||||
|
let tempStyle = '';
|
||||||
|
if (gpuTemp >= tempHigh) {
|
||||||
|
tempStyle = 'color: red; font-weight: bold;';
|
||||||
|
} else if (gpuTemp >= tempWarn) {
|
||||||
|
tempStyle = 'color: #FFC300; font-weight: bold;';
|
||||||
|
}
|
||||||
|
details.push(`Temp: <span style="${tempStyle}">${Ext.util.Format.number(gpuTemp, '0')}${tempUnit}</span>`);
|
||||||
}
|
}
|
||||||
details.push(`Temp: <span style="${tempStyle}">${stats.temperature.gpu}${stats.temperature.unit}</span>`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Power
|
// Power
|
||||||
@ -489,9 +514,12 @@ Ext.define('PVE.node.StatusView', {
|
|||||||
}
|
}
|
||||||
|
|
||||||
html += '</table>';
|
html += '</table>';
|
||||||
return html.indexOf('<tr>') > 0
|
if (html.indexOf('<tr>') <= 0) {
|
||||||
? '<div style="padding-left: 20px; box-sizing: border-box;">' + html + '</div>'
|
this.hide();
|
||||||
: '';
|
return '';
|
||||||
|
}
|
||||||
|
this.show();
|
||||||
|
return '<div style="padding-left: 20px; box-sizing: border-box;">' + html + '</div>';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -520,7 +548,9 @@ Ext.define('PVE.node.StatusView', {
|
|||||||
} catch(e) {
|
} catch(e) {
|
||||||
objValue = {};
|
objValue = {};
|
||||||
}
|
}
|
||||||
const tempHelper = Ext.create('PVE.mod.TempHelper', {srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: objValue.temp_unit === 'F' ? PVE.mod.TempHelper.FAHRENHEIT : PVE.mod.TempHelper.CELSIUS});
|
this.show();
|
||||||
|
const tempHelper = Ext.create('PVE.mod.TempHelper', {srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: value.temp_unit === 'F' ? PVE.mod.TempHelper.FAHRENHEIT : PVE.mod.TempHelper.CELSIUS});
|
||||||
|
const ignoreThreshold = tempHelper.getTemp(parseFloat(value.ignore_temp_below));
|
||||||
const drvKeys = Object.keys(objValue).filter(item => String(item).startsWith(addressPrefix)).sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }));
|
const drvKeys = Object.keys(objValue).filter(item => String(item).startsWith(addressPrefix)).sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }));
|
||||||
let drvData = [];
|
let drvData = [];
|
||||||
drvKeys.forEach((drvKey) => {
|
drvKeys.forEach((drvKey) => {
|
||||||
@ -536,7 +566,7 @@ Ext.define('PVE.node.StatusView', {
|
|||||||
tempCrit = tempHelper.getTemp(parseFloat(drv[sensorName][secondLevelKey]));
|
tempCrit = tempHelper.getTemp(parseFloat(drv[sensorName][secondLevelKey]));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!isNaN(tempVal)) {
|
if (!isNaN(tempVal) && tempVal >= ignoreThreshold) {
|
||||||
let tempStyle = '';
|
let tempStyle = '';
|
||||||
if (!isNaN(tempMax) && tempVal >= tempMax) {
|
if (!isNaN(tempMax) && tempVal >= tempMax) {
|
||||||
tempStyle = 'color: #FFC300; font-weight: bold;';
|
tempStyle = 'color: #FFC300; font-weight: bold;';
|
||||||
@ -600,7 +630,9 @@ Ext.define('PVE.node.StatusView', {
|
|||||||
} catch(e) {
|
} catch(e) {
|
||||||
objValue = {};
|
objValue = {};
|
||||||
}
|
}
|
||||||
const tempHelper = Ext.create('PVE.mod.TempHelper', {srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: objValue.temp_unit === 'F' ? PVE.mod.TempHelper.FAHRENHEIT : PVE.mod.TempHelper.CELSIUS});
|
this.show();
|
||||||
|
const tempHelper = Ext.create('PVE.mod.TempHelper', {srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: value.temp_unit === 'F' ? PVE.mod.TempHelper.FAHRENHEIT : PVE.mod.TempHelper.CELSIUS});
|
||||||
|
const ignoreThreshold = tempHelper.getTemp(parseFloat(value.ignore_temp_below));
|
||||||
const nvmeKeys = Object.keys(objValue).filter(item => String(item).startsWith(addressPrefix)).sort();
|
const nvmeKeys = Object.keys(objValue).filter(item => String(item).startsWith(addressPrefix)).sort();
|
||||||
let nvmeData = [];
|
let nvmeData = [];
|
||||||
nvmeKeys.forEach((nvmeKey, index) => {
|
nvmeKeys.forEach((nvmeKey, index) => {
|
||||||
@ -618,7 +650,7 @@ Ext.define('PVE.node.StatusView', {
|
|||||||
model = objValue[nvmeKey]['model'] || 'Unknown';
|
model = objValue[nvmeKey]['model'] || 'Unknown';
|
||||||
serial = objValue[nvmeKey]['serial'] || '';
|
serial = objValue[nvmeKey]['serial'] || '';
|
||||||
|
|
||||||
if (!isNaN(tempVal)) {
|
if (!isNaN(tempVal) && tempVal >= ignoreThreshold) {
|
||||||
let tempStyle = '';
|
let tempStyle = '';
|
||||||
if (!isNaN(tempMax) && tempVal >= tempMax) {
|
if (!isNaN(tempMax) && tempVal >= tempMax) {
|
||||||
tempStyle = 'color: #FFC300; font-weight: bold;';
|
tempStyle = 'color: #FFC300; font-weight: bold;';
|
||||||
@ -688,7 +720,9 @@ Ext.define('PVE.node.StatusView', {
|
|||||||
} catch(e) {
|
} catch(e) {
|
||||||
objValue = {};
|
objValue = {};
|
||||||
}
|
}
|
||||||
const tempHelper = Ext.create('PVE.mod.TempHelper', {srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: objValue.temp_unit === 'F' ? PVE.mod.TempHelper.FAHRENHEIT : PVE.mod.TempHelper.CELSIUS});
|
this.show();
|
||||||
|
const tempHelper = Ext.create('PVE.mod.TempHelper', {srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: value.temp_unit === 'F' ? PVE.mod.TempHelper.FAHRENHEIT : PVE.mod.TempHelper.CELSIUS});
|
||||||
|
const ignoreThreshold = tempHelper.getTemp(parseFloat(value.ignore_temp_below));
|
||||||
|
|
||||||
// Keep only keys that do not belong to known categories
|
// Keep only keys that do not belong to known categories
|
||||||
const otherKeys = Object.keys(objValue).filter(key =>
|
const otherKeys = Object.keys(objValue).filter(key =>
|
||||||
@ -723,7 +757,7 @@ Ext.define('PVE.node.StatusView', {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isNaN(tempVal)) {
|
if (!isNaN(tempVal) && tempVal >= ignoreThreshold) {
|
||||||
let tempStyle = '';
|
let tempStyle = '';
|
||||||
if (!isNaN(tempMax) && tempVal >= tempMax) {
|
if (!isNaN(tempMax) && tempVal >= tempMax) {
|
||||||
tempStyle = 'color: #FFC300; font-weight: bold;';
|
tempStyle = 'color: #FFC300; font-weight: bold;';
|
||||||
@ -788,6 +822,7 @@ Ext.define('PVE.node.StatusView', {
|
|||||||
} catch(e) {
|
} catch(e) {
|
||||||
objValue = {};
|
objValue = {};
|
||||||
}
|
}
|
||||||
|
this.show();
|
||||||
|
|
||||||
// Recursive function to find fan keys and values
|
// Recursive function to find fan keys and values
|
||||||
function findFanKeys(obj, fanKeys, parentKey = null) {
|
function findFanKeys(obj, fanKeys, parentKey = null) {
|
||||||
@ -846,6 +881,7 @@ Ext.define('PVE.node.StatusView', {
|
|||||||
this.hide();
|
this.hide();
|
||||||
return '';
|
return '';
|
||||||
} else if (!gpuStats || !gpuStats.Graphics || !gpuStats.Graphics.NVIDIA) {
|
} else if (!gpuStats || !gpuStats.Graphics || !gpuStats.Graphics.NVIDIA) {
|
||||||
|
this.hide();
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -873,198 +909,307 @@ Ext.define('PVE.node.StatusView', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
|
this.hide();
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
this.show();
|
||||||
|
|
||||||
return '<div style="padding-left: 20px; box-sizing: border-box;"><table style="width: 100%; border-collapse: collapse; table-layout: fixed;">' + rows.join('') + '</table></div>';
|
return '<div style="padding-left: 20px; box-sizing: border-box;"><table style="width: 100%; border-collapse: collapse; table-layout: fixed;">' + rows.join('') + '</table></div>';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
itemId: 'upsc',
|
itemId: 'upsc',
|
||||||
colspan: 2,
|
colspan: 2,
|
||||||
printBar: false,
|
title: gettext('UPS Status'),
|
||||||
title: gettext('UPS Status'),
|
iconCls: 'fa fa-fw fa-battery-three-quarters',
|
||||||
iconCls: 'fa fa-fw fa-battery-three-quarters',
|
valueField: 'PveMod_upsInfo',
|
||||||
textField: 'PveMod_upsInfo',
|
printBar: true,
|
||||||
renderer: function(value) {
|
warningThreshold: 1.1,
|
||||||
|
criticalThreshold: 1.2,
|
||||||
|
layout: {
|
||||||
|
type: 'vbox',
|
||||||
|
align: 'stretch',
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
xtype: 'component',
|
||||||
|
itemId: 'label',
|
||||||
|
data: {
|
||||||
|
title: '',
|
||||||
|
usage: '',
|
||||||
|
iconCls: undefined,
|
||||||
|
},
|
||||||
|
tpl: [
|
||||||
|
'<div class="left-aligned">',
|
||||||
|
'<tpl if="iconCls">',
|
||||||
|
'<i class="{iconCls}"></i> ',
|
||||||
|
'</tpl>',
|
||||||
|
'{title}</div>',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
xtype: 'container',
|
||||||
|
layout: {
|
||||||
|
type: 'hbox',
|
||||||
|
align: 'middle',
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
xtype: 'component',
|
||||||
|
itemId: 'usageText',
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
margin: '0 16 0 0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
xtype: 'container',
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
layout: {
|
||||||
|
type: 'vbox',
|
||||||
|
align: 'stretch',
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
xtype: 'component',
|
||||||
|
itemId: 'loadText',
|
||||||
|
margin: '0 0 2 0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
xtype: 'progressbar',
|
||||||
|
itemId: 'progress',
|
||||||
|
height: 5,
|
||||||
|
value: 0,
|
||||||
|
animate: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
initComponent: function() {
|
||||||
|
var me = this;
|
||||||
|
if (!me.title) {
|
||||||
|
throw 'no title defined';
|
||||||
|
}
|
||||||
|
Ext.container.Container.prototype.initComponent.call(me);
|
||||||
|
var progress = me.down('#progress');
|
||||||
|
if (progress) {
|
||||||
|
progress.setVisible(!!me.printBar);
|
||||||
|
}
|
||||||
|
me.updateValue(me.text, me.value);
|
||||||
|
me.setIconCls(me.iconCls);
|
||||||
|
},
|
||||||
|
setPrintBar: function(enable) {
|
||||||
|
var me = this;
|
||||||
|
me.printBar = enable;
|
||||||
|
var progress = me.down('#progress');
|
||||||
|
if (progress) {
|
||||||
|
progress.setVisible(enable);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Single source of truth for all DOM writes — called once per refresh
|
||||||
|
// cycle, right after renderer() returns. renderer() only computes data;
|
||||||
|
// it never touches the DOM, so there's no race/flash between the two.
|
||||||
|
updateValue: function(text, usage) {
|
||||||
|
var me = this;
|
||||||
|
var loadText = me._pendingLoadText || '';
|
||||||
|
|
||||||
|
if (me.lastText === text && me.lastUsage === usage && me.lastLoadText === loadText) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
me.lastText = text;
|
||||||
|
me.lastUsage = usage;
|
||||||
|
me.lastLoadText = loadText;
|
||||||
|
|
||||||
|
var label = me.getComponent('label');
|
||||||
|
if (label) {
|
||||||
|
label.update(Ext.apply(label.data, { title: me.title, usage: '' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
var usageText = me.down('#usageText');
|
||||||
|
if (usageText) {
|
||||||
|
if (usageText.setHtml) {
|
||||||
|
usageText.setHtml(text || '');
|
||||||
|
} else {
|
||||||
|
usageText.update(text || '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var loadTextCmp = me.down('#loadText');
|
||||||
|
if (loadTextCmp) {
|
||||||
|
if (loadTextCmp.setHtml) {
|
||||||
|
loadTextCmp.setHtml(loadText);
|
||||||
|
} else {
|
||||||
|
loadTextCmp.update(loadText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var progressBar = me.down('#progress');
|
||||||
|
if (usage !== undefined && me.printBar && Ext.isNumeric(usage) && usage >= 0 && progressBar) {
|
||||||
|
progressBar.updateProgress(usage, '');
|
||||||
|
if (usage > me.criticalThreshold) {
|
||||||
|
progressBar.removeCls('warning');
|
||||||
|
progressBar.addCls('critical');
|
||||||
|
} else if (usage > me.warningThreshold) {
|
||||||
|
progressBar.removeCls('critical');
|
||||||
|
progressBar.addCls('warning');
|
||||||
|
} else {
|
||||||
|
progressBar.removeCls('warning');
|
||||||
|
progressBar.removeCls('critical');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Drives the progress bar's fill — based on battery.charge.
|
||||||
|
calculate: function(used) {
|
||||||
|
if (!used || used.disabled === true || typeof used !== 'object') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let charge = NaN;
|
||||||
|
Object.keys(used).forEach(function(k) {
|
||||||
|
const row = used[k];
|
||||||
|
if (row && typeof row === 'object' && row['battery.charge'] != null) {
|
||||||
|
charge = parseFloat(row['battery.charge']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (isNaN(charge)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Math.max(0, Math.min(1, charge / 100));
|
||||||
|
},
|
||||||
|
// Pure computation — no DOM writes. Stashes the "Battery capacity ...
|
||||||
|
// X% (Runtime: ...)" line for updateValue to place above the bar, and
|
||||||
|
// returns a 30/70 table (model | other info incl. Load) wrapped in the
|
||||||
|
// standard indent div, matching the other widgets in this panel.
|
||||||
|
renderer: function(value) {
|
||||||
let objValue;
|
let objValue;
|
||||||
try {
|
try {
|
||||||
objValue = value || {};
|
objValue = value || {};
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
objValue = {};
|
objValue = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (objValue.disabled === true) {
|
if (objValue.disabled === true) {
|
||||||
this.hide();
|
this.hide();
|
||||||
|
this.setPrintBar(false);
|
||||||
return '';
|
return '';
|
||||||
} else if (!objValue || Object.keys(objValue).length === 0) {
|
|
||||||
return 'N/A';
|
|
||||||
}
|
}
|
||||||
|
this.show();
|
||||||
|
|
||||||
// Helper function to get status color
|
const upsKeys = Object.keys(objValue).filter(function(k) {
|
||||||
function getStatusColor(status) {
|
return objValue[k] && typeof objValue[k] === 'object' && !Array.isArray(objValue[k]);
|
||||||
if (!status) return '#999';
|
});
|
||||||
const statusUpper = status.toUpperCase();
|
if (!upsKeys.length) {
|
||||||
if (statusUpper.includes('OL')) return null;
|
this.hide();
|
||||||
if (statusUpper.includes('OB')) return '#d9534f';
|
this.setPrintBar(false);
|
||||||
if (statusUpper.includes('LB')) return '#d9534f';
|
return '';
|
||||||
return '#f0ad4e';
|
|
||||||
}
|
}
|
||||||
|
this.show();
|
||||||
|
this.setPrintBar(true);
|
||||||
|
|
||||||
// Helper function to get load/charge color
|
|
||||||
function getPercentageColor(value, isLoad = false) {
|
|
||||||
if (!value || isNaN(value)) return '#999';
|
|
||||||
const num = parseFloat(value);
|
|
||||||
if (isLoad) {
|
|
||||||
if (num >= 80) return '#d9534f';
|
|
||||||
if (num >= 60) return '#f0ad4e';
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
if (num <= 20) return '#d9534f';
|
|
||||||
if (num <= 50) return '#f0ad4e';
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to format runtime
|
|
||||||
function formatRuntime(seconds) {
|
function formatRuntime(seconds) {
|
||||||
if (!seconds || isNaN(seconds)) return 'N/A';
|
const s = parseInt(seconds, 10);
|
||||||
const mins = Math.floor(seconds / 60);
|
if (!s || isNaN(s)) {
|
||||||
const secs = seconds % 60;
|
return null;
|
||||||
return `${mins}m ${secs}s`;
|
}
|
||||||
|
const h = Math.floor(s / 3600);
|
||||||
|
const m = Math.floor((s % 3600) / 60);
|
||||||
|
if (h > 0) {
|
||||||
|
return h + 'h ' + m + 'm';
|
||||||
|
}
|
||||||
|
return m + 'm';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process each UPS in the data
|
function statusText(upsStatus) {
|
||||||
let allDisplayItems = [];
|
const u = String(upsStatus || '').toUpperCase();
|
||||||
|
if (u.indexOf('LB') >= 0) {
|
||||||
|
return { text: 'Low Battery', color: '#d9534f' };
|
||||||
|
}
|
||||||
|
if (u.indexOf('OB') >= 0) {
|
||||||
|
return { text: 'On Battery', color: '#d9534f' };
|
||||||
|
}
|
||||||
|
if (u.indexOf('FSD') >= 0) {
|
||||||
|
return { text: 'Shutdown', color: '#d9534f' };
|
||||||
|
}
|
||||||
|
if (u.indexOf('OL') >= 0) {
|
||||||
|
return {
|
||||||
|
text: u.indexOf('CHRG') >= 0 ? 'Online, charging' : 'Online',
|
||||||
|
color: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { text: upsStatus || 'Unknown', color: '#f0ad4e' };
|
||||||
|
}
|
||||||
|
|
||||||
Object.keys(objValue).forEach(upsKey => {
|
function colorize(label, color) {
|
||||||
const upsData = objValue[upsKey];
|
if (!color) {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
return '<span style="color:' + color + ';">' + label + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
// Extract key UPS information
|
let aboveBarText = '';
|
||||||
const batteryCharge = upsData['battery.charge'];
|
const rows = [];
|
||||||
const batteryRuntime = upsData['battery.runtime'];
|
|
||||||
const inputVoltage = upsData['input.voltage'];
|
upsKeys.forEach(function(upsKey) {
|
||||||
const upsLoad = upsData['ups.load'];
|
const upsData = objValue[upsKey] || {};
|
||||||
const upsStatus = upsData['ups.status'];
|
const charge = parseFloat(upsData['battery.charge']);
|
||||||
const upsModel = upsData['ups.model'] || upsData['device.model'];
|
const runtime = formatRuntime(upsData['battery.runtime']);
|
||||||
|
const inputVoltage = parseFloat(upsData['input.voltage']);
|
||||||
|
const load = parseFloat(upsData['ups.load']);
|
||||||
|
const watts = parseFloat(upsData['ups.realpower']);
|
||||||
|
const model = upsData['ups.model'] || upsData['device.model'] || upsKey;
|
||||||
|
const st = statusText(upsData['ups.status']);
|
||||||
const testResult = upsData['ups.test.result'];
|
const testResult = upsData['ups.test.result'];
|
||||||
const batteryChargeLow = upsData['battery.charge.low'];
|
const manufacturingDate = upsData['battery.mfr.date'];
|
||||||
const batteryRuntimeLow = upsData['battery.runtime.low'];
|
|
||||||
const upsRealPowerNominal = upsData['ups.realpower.nominal'];
|
|
||||||
const batteryMfrDate = upsData['battery.mfr.date'];
|
|
||||||
|
|
||||||
// Main status line with all metrics
|
// Above the bar: "Battery capacity" on the left, charge% (Runtime: ...) on the right.
|
||||||
let statusLine = '';
|
let rightSide = !isNaN(charge) ? (Math.round(charge) + '%') : '';
|
||||||
|
if (runtime) {
|
||||||
|
rightSide += (rightSide ? ' ' : '') + '(Runtime: ' + runtime + ' left)';
|
||||||
|
}
|
||||||
|
aboveBarText =
|
||||||
|
'<div style="display: flex; justify-content: space-between; gap: 8px;">' +
|
||||||
|
'<span>Battery capacity</span>' +
|
||||||
|
'<span style="text-align: right;">' + rightSide + '</span>' +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
// Status
|
// General information table: Status, Output, Input, Load, Test.
|
||||||
if (upsStatus) {
|
const infoBits = [];
|
||||||
const statusUpper = upsStatus.toUpperCase();
|
if (st.text) {
|
||||||
let statusText = 'Unknown';
|
infoBits.push('Status: ' + colorize(st.text, st.color));
|
||||||
let statusColor = '#f0ad4e';
|
}
|
||||||
|
if (!isNaN(watts)) {
|
||||||
if (statusUpper.includes('OL')) {
|
infoBits.push('Output: ' + Math.round(watts) + 'W');
|
||||||
statusText = 'Online';
|
}
|
||||||
statusColor = null;
|
if (!isNaN(inputVoltage)) {
|
||||||
} else if (statusUpper.includes('OB')) {
|
const places = inputVoltage >= 50 ? 0 : 1;
|
||||||
statusText = 'On Battery';
|
infoBits.push('Input: ' + inputVoltage.toFixed(places) + ' V');
|
||||||
statusColor = '#d9534f';
|
}
|
||||||
} else if (statusUpper.includes('LB')) {
|
if (!isNaN(load)) {
|
||||||
statusText = 'Low Battery';
|
infoBits.push('Load: ' + Math.round(load) + '%');
|
||||||
statusColor = '#d9534f';
|
}
|
||||||
} else {
|
if (manufacturingDate) {
|
||||||
statusText = upsStatus;
|
infoBits.push('Manufacturing Date: ' + manufacturingDate);
|
||||||
statusColor = '#f0ad4e';
|
}
|
||||||
}
|
if (testResult) {
|
||||||
|
infoBits.push('Test: ' + testResult);
|
||||||
let statusStyle = statusColor ? ('color: ' + statusColor + ';') : '';
|
|
||||||
statusLine += 'Status: <span style="' + statusStyle + '">' + statusText + '</span>';
|
|
||||||
} else {
|
|
||||||
statusLine += 'Status: <span>N/A</span>';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Battery charge
|
rows.push(
|
||||||
if (statusLine) statusLine += ' | ';
|
'<tr>' +
|
||||||
if (batteryCharge) {
|
'<td style="padding: 2px 10px 2px 0; text-align: left; width: 30%; vertical-align: top; overflow-wrap: anywhere; word-break: break-word;">' + model + '</td>' +
|
||||||
const chargeColor = getPercentageColor(batteryCharge, false);
|
'<td style="padding: 2px 0 2px 10px; text-align: right; width: 70%; vertical-align: top; overflow-wrap: anywhere; word-break: break-word; white-space: normal;">' + infoBits.join(' | ') + '</td>' +
|
||||||
let chargeStyle = chargeColor ? ('color: ' + chargeColor + ';') : '';
|
'</tr>'
|
||||||
statusLine += 'Battery: <span style="' + chargeStyle + '">' + batteryCharge + '%</span>';
|
);
|
||||||
} else {
|
|
||||||
statusLine += 'Battery: <span>N/A</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load percentage
|
|
||||||
if (statusLine) statusLine += ' | ';
|
|
||||||
if (upsLoad) {
|
|
||||||
const loadColor = getPercentageColor(upsLoad, true);
|
|
||||||
let loadStyle = loadColor ? ('color: ' + loadColor + ';') : '';
|
|
||||||
statusLine += 'Load: <span style="' + loadStyle + '">' + upsLoad + '%</span>';
|
|
||||||
} else {
|
|
||||||
statusLine += 'Load: <span>N/A</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Runtime
|
|
||||||
if (statusLine) statusLine += ' | ';
|
|
||||||
if (batteryRuntime) {
|
|
||||||
const runtime = parseInt(batteryRuntime);
|
|
||||||
const runtimeLowThreshold = batteryRuntimeLow ? parseInt(batteryRuntimeLow) : 600;
|
|
||||||
let runtimeColor = null;
|
|
||||||
if (runtime <= runtimeLowThreshold / 2) runtimeColor = '#d9534f';
|
|
||||||
else if (runtime <= runtimeLowThreshold) runtimeColor = '#f0ad4e';
|
|
||||||
let runtimeStyle = runtimeColor ? ('color: ' + runtimeColor + ';') : '';
|
|
||||||
statusLine += 'Runtime: <span style="' + runtimeStyle + '">' + formatRuntime(runtime) + '</span>';
|
|
||||||
} else {
|
|
||||||
statusLine += 'Runtime: <span>N/A</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Input voltage
|
|
||||||
if (statusLine) statusLine += ' | ';
|
|
||||||
if (inputVoltage) {
|
|
||||||
statusLine += 'Input: <span>' + parseFloat(inputVoltage).toFixed(0) + 'V</span>';
|
|
||||||
} else {
|
|
||||||
statusLine += 'Input: <span>N/A</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate actual watt usage
|
|
||||||
if (statusLine) statusLine += ' | ';
|
|
||||||
let actualWattage = null;
|
|
||||||
if (upsLoad && upsRealPowerNominal) {
|
|
||||||
const load = parseFloat(upsLoad);
|
|
||||||
const nominal = parseFloat(upsRealPowerNominal);
|
|
||||||
if (!isNaN(load) && !isNaN(nominal)) {
|
|
||||||
actualWattage = Math.round((load / 100) * nominal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Real power (calculated watt usage)
|
|
||||||
if (actualWattage !== null) {
|
|
||||||
statusLine += 'Output: <span>' + actualWattage + 'W</span>';
|
|
||||||
} else {
|
|
||||||
statusLine += 'Output: <span>N/A</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append battery MFD + last test to the same line (single-line UPS summary)
|
|
||||||
statusLine += ' | Battery MFD: ' + (batteryMfrDate || 'N/A');
|
|
||||||
if (testResult && !testResult.toLowerCase().includes('no test')) {
|
|
||||||
const testColor = testResult.toLowerCase().includes('passed') ? null : '#d9534f';
|
|
||||||
let testStyle = testColor ? ('color: ' + testColor + ';') : '';
|
|
||||||
statusLine += ' | <span style="' + testStyle + '">Test: ' + testResult + '</span>';
|
|
||||||
} else {
|
|
||||||
statusLine += ' | Test: N/A';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build UPS display with model on left, details on right
|
|
||||||
let upsHtml = '<tr>';
|
|
||||||
upsHtml += '<td style="padding: 2px 10px 2px 0; text-align: left; width: 30%; vertical-align: top; overflow-wrap: anywhere; word-break: break-word;">' + (upsModel || upsKey) + '</td>';
|
|
||||||
upsHtml += '<td style="padding: 2px 0 2px 10px; text-align: right; width: 70%; vertical-align: top; overflow-wrap: anywhere; word-break: break-word; white-space: normal;">' + statusLine + '</td>';
|
|
||||||
upsHtml += '</tr>';
|
|
||||||
|
|
||||||
allDisplayItems.push(upsHtml);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Format the final output for all UPS devices
|
// Stash for updateValue to consume — no DOM writes here.
|
||||||
return '<div style="padding-left: 20px; box-sizing: border-box;"><table style="width: 100%; border-collapse: collapse; table-layout: fixed;">' + allDisplayItems.join('') + '</table></div>';
|
this._pendingLoadText = aboveBarText;
|
||||||
|
|
||||||
|
// Becomes the `text` argument passed to updateValue (left column table).
|
||||||
|
return '<div style="padding-left: 20px; box-sizing: border-box;"><table style="width: 100%; border-collapse: collapse; table-layout: fixed;">' + rows.join('') + '</table></div>';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
xtype: 'box',
|
xtype: 'box',
|
||||||
colspan: 2,
|
colspan: 2,
|
||||||
@ -1132,6 +1277,7 @@ Ext.define('PVE.node.StatusView', {
|
|||||||
this.hide();
|
this.hide();
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
this.show();
|
||||||
|
|
||||||
const titleMap = {
|
const titleMap = {
|
||||||
manufacturer: 'Manufacturer',
|
manufacturer: 'Manufacturer',
|
||||||
|
|||||||
@ -15,6 +15,7 @@ our @EXPORT_OK = qw(
|
|||||||
debug
|
debug
|
||||||
read_sysfs
|
read_sysfs
|
||||||
is_process_alive
|
is_process_alive
|
||||||
|
get_process_ppid
|
||||||
read_lock_pid
|
read_lock_pid
|
||||||
acquire_exclusive_lock
|
acquire_exclusive_lock
|
||||||
ensure_pve_mod_directory_exists
|
ensure_pve_mod_directory_exists
|
||||||
@ -91,7 +92,33 @@ sub read_sysfs {
|
|||||||
|
|
||||||
sub is_process_alive {
|
sub is_process_alive {
|
||||||
my ($pid) = @_;
|
my ($pid) = @_;
|
||||||
return -d "/proc/$pid";
|
return 0 unless -d "/proc/$pid";
|
||||||
|
# A zombie still has /proc/<pid> but is dead - otherwise a stale
|
||||||
|
# worker lock is never cleaned up.
|
||||||
|
if (open my $fh, '<', "/proc/$pid/stat") {
|
||||||
|
my $line = <$fh>;
|
||||||
|
close $fh;
|
||||||
|
return 0 if defined $line && $line =~ /\)\s+Z\s/;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Returns the parent PID of $pid, or undef if it can't be determined.
|
||||||
|
sub get_process_ppid {
|
||||||
|
my ($pid) = @_;
|
||||||
|
return undef unless open my $fh, '<', "/proc/$pid/stat";
|
||||||
|
my $line = <$fh>;
|
||||||
|
close $fh;
|
||||||
|
return undef unless defined $line;
|
||||||
|
|
||||||
|
# comm field (2nd, in parens) can itself contain ')', so split on the
|
||||||
|
# last one before parsing the remaining space-separated fields.
|
||||||
|
my $last_paren = rindex($line, ')');
|
||||||
|
return undef if $last_paren < 0;
|
||||||
|
|
||||||
|
my @fields = split ' ', substr($line, $last_paren + 1);
|
||||||
|
return undef unless defined $fields[1] && $fields[1] =~ /^(\d+)$/;
|
||||||
|
return $1;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub read_lock_pid {
|
sub read_lock_pid {
|
||||||
|
|||||||
@ -17,7 +17,6 @@ enable_hdd_temp=0
|
|||||||
enable_nvme_temp=0
|
enable_nvme_temp=0
|
||||||
enable_fan_speed=0
|
enable_fan_speed=0
|
||||||
display_zero_speed_fans=0
|
display_zero_speed_fans=0
|
||||||
temp_unit=C
|
|
||||||
|
|
||||||
[ups]
|
[ups]
|
||||||
enabled=0
|
enabled=0
|
||||||
@ -26,6 +25,8 @@ device_name=ups@localhost
|
|||||||
[system_info]
|
[system_info]
|
||||||
enabled=0
|
enabled=0
|
||||||
type=1
|
type=1
|
||||||
|
temp_unit=C
|
||||||
|
ignore_temp_below=5
|
||||||
|
|
||||||
# Debug mode: when a collector's mode is 1, the real tool is not required.
|
# Debug mode: when a collector's mode is 1, the real tool is not required.
|
||||||
# Data is read from the file path instead. Useful for development/testing.
|
# Data is read from the file path instead. Useful for development/testing.
|
||||||
|
|||||||
@ -65,11 +65,11 @@ node_info_defaults() {
|
|||||||
LM_SENSORS_ENABLED=0
|
LM_SENSORS_ENABLED=0
|
||||||
ENABLE_CPU=0; CPU_TEMP_TARGET="Core"
|
ENABLE_CPU=0; CPU_TEMP_TARGET="Core"
|
||||||
ENABLE_RAM_TEMP=0; ENABLE_HDD_TEMP=0; ENABLE_NVME_TEMP=0; ENABLE_OTHER_TEMP=0
|
ENABLE_RAM_TEMP=0; ENABLE_HDD_TEMP=0; ENABLE_NVME_TEMP=0; ENABLE_OTHER_TEMP=0
|
||||||
ENABLE_FAN_SPEED=0; DISPLAY_ZERO_SPEED_FANS=0; TEMP_UNIT="C"
|
ENABLE_FAN_SPEED=0; DISPLAY_ZERO_SPEED_FANS=0
|
||||||
ENABLE_INTEL_GPU_INFO=0; ENABLE_NVIDIA_GPU_INFO=0; ENABLE_AMD_GPU_INFO=0
|
ENABLE_INTEL_GPU_INFO=0; ENABLE_NVIDIA_GPU_INFO=0; ENABLE_AMD_GPU_INFO=0
|
||||||
ENABLE_GPU_HISTORY=0
|
ENABLE_GPU_HISTORY=0
|
||||||
ENABLE_UPS=0; UPS_DEVICE_NAME="ups@localhost"
|
ENABLE_UPS=0; UPS_DEVICE_NAME="ups@localhost"
|
||||||
ENABLE_SYSTEM_INFO=0; SYSTEM_INFO_TYPE=1
|
ENABLE_SYSTEM_INFO=0; SYSTEM_INFO_TYPE=1; TEMP_UNIT="C"; IGNORE_TEMP_BELOW=5
|
||||||
DEBUG_LM_SENSORS=0; DEBUG_LM_SENSORS_FILE="/tmp/sensors-output.json"
|
DEBUG_LM_SENSORS=0; DEBUG_LM_SENSORS_FILE="/tmp/sensors-output.json"
|
||||||
DEBUG_INTEL=0; DEBUG_INTEL_FILE="/tmp/intel-gpu-devices.txt"
|
DEBUG_INTEL=0; DEBUG_INTEL_FILE="/tmp/intel-gpu-devices.txt"
|
||||||
DEBUG_INTEL_OUTPUT_FILE="/tmp/intel-gpu-top-output.txt"
|
DEBUG_INTEL_OUTPUT_FILE="/tmp/intel-gpu-top-output.txt"
|
||||||
@ -104,11 +104,12 @@ node_info_load_conf() {
|
|||||||
lm_sensors.enable_other_temp) ENABLE_OTHER_TEMP="$val" ;;
|
lm_sensors.enable_other_temp) ENABLE_OTHER_TEMP="$val" ;;
|
||||||
lm_sensors.enable_fan_speed) ENABLE_FAN_SPEED="$val" ;;
|
lm_sensors.enable_fan_speed) ENABLE_FAN_SPEED="$val" ;;
|
||||||
lm_sensors.display_zero_speed_fans) DISPLAY_ZERO_SPEED_FANS="$val" ;;
|
lm_sensors.display_zero_speed_fans) DISPLAY_ZERO_SPEED_FANS="$val" ;;
|
||||||
lm_sensors.temp_unit) TEMP_UNIT="$val" ;;
|
|
||||||
ups.enabled) ENABLE_UPS="$val" ;;
|
ups.enabled) ENABLE_UPS="$val" ;;
|
||||||
ups.device_name) UPS_DEVICE_NAME="$val" ;;
|
ups.device_name) UPS_DEVICE_NAME="$val" ;;
|
||||||
system_info.enabled) ENABLE_SYSTEM_INFO="$val" ;;
|
system_info.enabled) ENABLE_SYSTEM_INFO="$val" ;;
|
||||||
system_info.type) SYSTEM_INFO_TYPE="$val" ;;
|
system_info.type) SYSTEM_INFO_TYPE="$val" ;;
|
||||||
|
system_info.temp_unit) TEMP_UNIT="$val" ;;
|
||||||
|
system_info.ignore_temp_below) IGNORE_TEMP_BELOW="$val" ;;
|
||||||
debug.lm_sensors_mode) DEBUG_LM_SENSORS="$val" ;;
|
debug.lm_sensors_mode) DEBUG_LM_SENSORS="$val" ;;
|
||||||
debug.lm_sensors_output_file) DEBUG_LM_SENSORS_FILE="$val" ;;
|
debug.lm_sensors_output_file) DEBUG_LM_SENSORS_FILE="$val" ;;
|
||||||
debug.intel_mode) DEBUG_INTEL="$val" ;;
|
debug.intel_mode) DEBUG_INTEL="$val" ;;
|
||||||
@ -133,11 +134,11 @@ node_info_configure() {
|
|||||||
LM_SENSORS_ENABLED=0
|
LM_SENSORS_ENABLED=0
|
||||||
ENABLE_CPU=0; CPU_TEMP_TARGET="Core"
|
ENABLE_CPU=0; CPU_TEMP_TARGET="Core"
|
||||||
ENABLE_RAM_TEMP=0; ENABLE_HDD_TEMP=0; ENABLE_NVME_TEMP=0; ENABLE_OTHER_TEMP=0
|
ENABLE_RAM_TEMP=0; ENABLE_HDD_TEMP=0; ENABLE_NVME_TEMP=0; ENABLE_OTHER_TEMP=0
|
||||||
ENABLE_FAN_SPEED=0; DISPLAY_ZERO_SPEED_FANS=0; TEMP_UNIT="C"
|
ENABLE_FAN_SPEED=0; DISPLAY_ZERO_SPEED_FANS=0
|
||||||
ENABLE_INTEL_GPU_INFO=0; ENABLE_NVIDIA_GPU_INFO=0; ENABLE_AMD_GPU_INFO=0
|
ENABLE_INTEL_GPU_INFO=0; ENABLE_NVIDIA_GPU_INFO=0; ENABLE_AMD_GPU_INFO=0
|
||||||
ENABLE_GPU_HISTORY=0
|
ENABLE_GPU_HISTORY=0
|
||||||
ENABLE_UPS=0; UPS_DEVICE_NAME="ups@localhost"
|
ENABLE_UPS=0; UPS_DEVICE_NAME="ups@localhost"
|
||||||
ENABLE_SYSTEM_INFO=0; SYSTEM_INFO_TYPE=1
|
ENABLE_SYSTEM_INFO=0; SYSTEM_INFO_TYPE=1; TEMP_UNIT="C"; IGNORE_TEMP_BELOW=5
|
||||||
|
|
||||||
local lm_sensors_ok=false
|
local lm_sensors_ok=false
|
||||||
local sensors_detected=false
|
local sensors_detected=false
|
||||||
@ -294,6 +295,26 @@ node_info_configure() {
|
|||||||
[fF]) TEMP_UNIT="F"; info "Using Fahrenheit." ;;
|
[fF]) TEMP_UNIT="F"; info "Using Fahrenheit." ;;
|
||||||
*) TEMP_UNIT="C"; info "Using Celsius." ;;
|
*) TEMP_UNIT="C"; info "Using Celsius." ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
msgb "\n=== Ignore threshold ==="
|
||||||
|
local default_c=5 default_display entered
|
||||||
|
if [[ "$TEMP_UNIT" == "F" ]]; then
|
||||||
|
default_display=$(awk -v c="$default_c" 'BEGIN{printf "%.0f", c*9/5+32}')
|
||||||
|
else
|
||||||
|
default_display="$default_c"
|
||||||
|
fi
|
||||||
|
while true; do
|
||||||
|
entered=$(ask "Hide temperature readings below this value (°${TEMP_UNIT}) [${default_display}]")
|
||||||
|
[[ -z "$entered" ]] && entered="$default_display"
|
||||||
|
[[ "$entered" =~ ^-?[0-9]+(\.[0-9]+)?$ ]] && break
|
||||||
|
warn "Invalid number, please try again."
|
||||||
|
done
|
||||||
|
if [[ "$TEMP_UNIT" == "F" ]]; then
|
||||||
|
IGNORE_TEMP_BELOW=$(awk -v f="$entered" 'BEGIN{printf "%.1f", (f-32)*5/9}')
|
||||||
|
else
|
||||||
|
IGNORE_TEMP_BELOW="$entered"
|
||||||
|
fi
|
||||||
|
info "Temperature readings below ${entered}°${TEMP_UNIT} will be hidden."
|
||||||
fi
|
fi
|
||||||
#endregion Temperature unit
|
#endregion Temperature unit
|
||||||
fi
|
fi
|
||||||
@ -478,7 +499,6 @@ enable_nvme_temp=${ENABLE_NVME_TEMP}
|
|||||||
enable_other_temp=${ENABLE_OTHER_TEMP}
|
enable_other_temp=${ENABLE_OTHER_TEMP}
|
||||||
enable_fan_speed=${ENABLE_FAN_SPEED}
|
enable_fan_speed=${ENABLE_FAN_SPEED}
|
||||||
display_zero_speed_fans=${DISPLAY_ZERO_SPEED_FANS}
|
display_zero_speed_fans=${DISPLAY_ZERO_SPEED_FANS}
|
||||||
temp_unit=${TEMP_UNIT}
|
|
||||||
|
|
||||||
[ups]
|
[ups]
|
||||||
enabled=${ENABLE_UPS}
|
enabled=${ENABLE_UPS}
|
||||||
@ -487,6 +507,8 @@ device_name=${UPS_DEVICE_NAME}
|
|||||||
[system_info]
|
[system_info]
|
||||||
enabled=${ENABLE_SYSTEM_INFO}
|
enabled=${ENABLE_SYSTEM_INFO}
|
||||||
type=${SYSTEM_INFO_TYPE}
|
type=${SYSTEM_INFO_TYPE}
|
||||||
|
temp_unit=${TEMP_UNIT}
|
||||||
|
ignore_temp_below=${IGNORE_TEMP_BELOW}
|
||||||
|
|
||||||
# Debug mode: when a collector's mode is 1, the real tool is not required.
|
# Debug mode: when a collector's mode is 1, the real tool is not required.
|
||||||
# Data is read from the file path instead. Useful for development/testing.
|
# Data is read from the file path instead. Useful for development/testing.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user