Compare commits

..

No commits in common. "main" and "v1.0.6" have entirely different histories.
main ... v1.0.6

16 changed files with 246 additions and 716 deletions

View File

@ -58,6 +58,7 @@ jobs:
if [[ "${{ github.event_name }}" == "push" ]]; then
# Always recreate from main — avoids rebase conflicts after PR merge
git checkout -b chore/version-bump origin/main
git push --force origin chore/version-bump
else
# Label change: check out the existing branch
git fetch origin chore/version-bump
@ -132,8 +133,8 @@ jobs:
echo "$MAIN_CHANGELOG"
} > debian/changelog
git add debian/changelog
git commit -m "chore: update changelog to v${NEW_VERSION}"
git push --force origin chore/version-bump
git diff --cached --quiet || git commit -m "chore: update changelog to v${NEW_VERSION}"
git push --force-with-lease origin chore/version-bump
- name: Build PR body
env:

View File

@ -36,20 +36,11 @@ jobs:
test:
needs: discover
runs-on: ubuntu-latest
container:
image: debian:trixie
strategy:
fail-fast: false
matrix:
mod: ${{ fromJSON(needs.discover.outputs.mods) }}
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
with:
fetch-depth: 0
@ -57,32 +48,22 @@ jobs:
# ── Build (mirrors test-release.yml) ───────────────────────────────────
- name: Install build deps
run: |
apt-get update -qq
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"
sudo apt-get update -qq
sudo apt-get install -y devscripts debhelper build-essential
- name: Inject version into Config.pm
run: |
BUILD_VERSION="${{ steps.version.outputs.build_version }}"
sed -i -E "s/^our \\\$VERSION\s*=.*/our \\\$VERSION = '${BUILD_VERSION}';/" \
src/modules/node_info/files/Config.pm
- name: Set package version in changelog
run: |
BUILD_VERSION="${{ steps.version.outputs.build_version }}"
sed -i -E "0,/\(([^)]*)\)/s//(${BUILD_VERSION})/" debian/changelog
VERSION=$(grep -m1 '(' debian/changelog | sed 's/.*(\(.*\)).*/\1/')
SHORT_SHA=$(git rev-parse --short HEAD)
export BUILD_VERSION="${VERSION}-test-${SHORT_SHA}"
perl -i -pe 'BEGIN { $v = $ENV{BUILD_VERSION} }
s/(our \$VERSION\s*=\s*'"'"')[^'"'"']*'"'"';/$1'"'"'$v'"'"';/
' src/modules/node_info/files/Config.pm
- name: Generate dynamic debian rules
run: |
bash build/gen-rules.sh >> debian/rules
bash build/gen-rules.sh conffiles >> debian/pve-mod.conffiles
- name: Build package
run: dpkg-buildpackage -us -uc -b
@ -91,26 +72,13 @@ jobs:
- name: Fetch upstream Proxmox files
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
run: apt-get install -y ../pve-mod_*.deb
run: sudo apt-get install -y ../pve-mod_*.deb
- name: Test mod
run: |
set -o pipefail
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 }}"
sudo bash build/test/test-patches.sh "${{ matrix.mod }}" 2>&1 | tee test-output.log
- name: Report failure as issue
if: failure()
@ -122,10 +90,6 @@ jobs:
{
echo "The weekly patch test failed for mod \`${MOD}\` against the latest Proxmox VE files."
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 ""
echo "<details><summary>Test log</summary>"
@ -145,10 +109,9 @@ jobs:
--jq ".[] | select(.title==\"$TITLE\") | .number" | head -n1)
if [[ -n "$EXISTING" ]]; then
gh issue edit "$EXISTING" --body-file issue-body.md
gh issue comment "$EXISTING" --body-file issue-body.md
echo "Updated and commented on existing issue #$EXISTING"
echo "Commented on existing issue #$EXISTING"
else
gh issue create --title "$TITLE" --label patch-break --body-file issue-body.md
echo "Opened new issue for mod $MOD"
fi
fi

View File

@ -121,7 +121,7 @@ test_one_mod() {
if [[ $ok -eq 0 ]]; then
info "Reverting patches for '$mod'..."
local revert_log
revert_log="$("$REVERT" "$mod" 2>&1)"
revert_log="$("$REVERT" 2>&1)"
echo "$revert_log"
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'"

17
debian/changelog vendored
View File

@ -1,20 +1,3 @@
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
* c21b973 remove buildtest file (#288)

View File

@ -1,4 +1,3 @@
interest-noawait /usr/share/pve-manager/js
interest-noawait /usr/share/perl5/PVE
interest-noawait /usr/share/pve-yew-mobile-gui
interest-noawait /usr/share/javascript/proxmox-widget-toolkit

View File

@ -3,80 +3,11 @@ A small collection of scripts and mods for Proxmox Virtual Environment (PVE)
If you find this helpful, a small donation is appreciated, [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=K8XPMSEBERH3W).
| 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
![Proxmox temp mod](https://github.com/Meliox/PVE-mods/blob/main/pve-mod-sensors.png?raw=true)
## Node sensor readings view
Compatibility:
- 9.0-9.2. Newer versions may often work
- Older version (7.x-8.x), use git version from Apr 6th 2025
![Promxox temp mod](https://github.com/Meliox/PVE-mods/blob/main/pve-mod-sensors.png?raw=true)
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:
@ -84,7 +15,7 @@ The following readings are possible:
- UPS information via Network Monitoring Tool
- Motherboard information or system information via dmidecode
#### How it works
### How it works
The modification involves the following steps:
1. Backup original files in the home directory/backup
- `/usr/share/pve-manager/js/pvemanagerlib.js`
@ -107,7 +38,7 @@ Notes:
- 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.
#### Install
### Install
Instructions be performed as 'root', as normal users do not have access to the files.
```
@ -120,7 +51,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.
### Nag screen deactivation
## Nag screen deactivation
(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.
@ -134,21 +65,21 @@ The script provides three options:
| `install` | Installs the modification by applying the necessary changes. |
| `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.
```
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
```
### Script to update all containers
## Script to update all containers
(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.
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.
#### Install
### Install
```
wget https://raw.githubusercontent.com/Meliox/PVE-mods/refs/heads/main/legacy-scripts/updateallcontainers.sh
```

View File

@ -1,7 +1,7 @@
--- /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.orig 2026-08-23 01:17:11.563403975 +0200
+++ /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js 2026-08-23 01:19:00.972084488 +0200
@@ -612,37 +612,8 @@
),
--- a/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js
+++ b/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js
@@ -599,37 +599,8 @@
Ext.Msg.alert(gettext('Error'), res.htmlStatus || res.result.message),
checked_command: function (orig_cmd) {
- Proxmox.Utils.API2Request({

View File

@ -97,9 +97,7 @@ sub _get_temperature_sensors {
fans => $config{lm_sensors}{enable_fan_speed} ? \1 : \0,
hdd => $config{lm_sensors}{enable_hdd_temp} ? \1 : \0,
other => $config{lm_sensors}{enable_other_temp} ? \1 : \0,
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,
temp_unit => $config{lm_sensors}{temp_unit},
data => { 'PVE MOD lm-sensors Enhanced' => $sensors_json },
});

View File

@ -112,8 +112,6 @@ sub _parse_upsc_output {
}
}
}
_normalize_ups_data($ups_data);
};
if ($@) {
debug(__LINE__, "Error parsing upsc output: $@");
@ -124,28 +122,4 @@ sub _parse_upsc_output {
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;

View File

@ -72,6 +72,7 @@ our %config = (
enable_other_temp => 0,
enable_fan_speed => 0,
display_zero_speed_fans => 0,
temp_unit => 'C',
},
ups => {
enabled => 0,
@ -80,8 +81,6 @@ our %config = (
system_info => {
enabled => 0,
type => 1, # 1 = System (dmidecode -t 1), 2 = Baseboard/Motherboard (dmidecode -t 2)
temp_unit => 'C',
ignore_temp_below => 5,
},
paths => {
working_dir => '/run/pveproxy/pve-mod',

View File

@ -4,7 +4,7 @@ use strict;
use warnings;
use Exporter 'import';
use POSIX qw(WNOHANG setsid);
use POSIX qw(WNOHANG);
use File::Path qw(remove_tree);
use PVE::PVEMod::Config qw(
@ -13,7 +13,7 @@ use PVE::PVEMod::Config qw(
$pve_mod_worker_lock $startup_lock
);
use PVE::PVEMod::Utils qw(
debug is_process_alive get_process_ppid read_lock_pid
debug is_process_alive read_lock_pid
acquire_exclusive_lock ensure_pve_mod_directory_exists
check_executable startup_message
);
@ -112,37 +112,10 @@ sub notify_pve_mod_worker {
# ============================================================================
sub _worker_lock_file_exists {
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;
return -f $pve_mod_worker_lock;
}
# 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.
# Forks the worker process and records its PID in the lock file.
sub _pve_mod_worker {
debug(__LINE__, "_pve_mod_worker called");
@ -152,57 +125,34 @@ sub _pve_mod_worker {
print $pve_mod_worker_fh "$$\n";
close($pve_mod_worker_fh);
debug(__LINE__, "Forking intermediate process for pve_mod_worker");
my $intermediate_pid = fork();
debug(__LINE__, "Forking new pve_mod_worker process");
my $pve_mod_worker_pid = fork();
unless (defined $intermediate_pid) {
debug(__LINE__, "Failed to fork intermediate pve_mod_worker process: $!");
unlink($pve_mod_worker_lock);
unless (defined $pve_mod_worker_pid) {
debug(__LINE__, "Failed to fork pve_mod_worker process: $!");
return;
}
if ($intermediate_pid == 0) {
# Intermediate child: detach into its own session, fork the real
# worker, then exit immediately so the worker is reparented to init.
setsid();
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);
if ($pve_mod_worker_pid == 0) {
# Child
$0 = "pve_mod_worker_controller";
debug(__LINE__, "Child process forked, calling _pve_mod_keep_alive");
_pve_mod_keep_alive();
exit(0);
}
# Parent — reap the short-lived intermediate process immediately, so it
# never has a chance to become a zombie under us either.
waitpid($intermediate_pid, 0);
my $status = $? >> 8;
if ($status == 0) {
debug(__LINE__, "pve_mod_worker process started successfully");
# Parent — update lock file with real child PID
debug(__LINE__, "Forked pve_mod_worker process with PID $pve_mod_worker_pid");
if (open my $fh, '>', $pve_mod_worker_lock) {
print $fh "$pve_mod_worker_pid\n";
close $fh;
debug(__LINE__, "Wrote pve_mod_worker PID to lock file: $pve_mod_worker_lock");
} else {
debug(__LINE__, "Intermediate pve_mod_worker process exited with status $status");
debug(__LINE__, "Failed to write pve_mod_worker lock file: $!");
kill('TERM', $pve_mod_worker_pid);
}
debug(__LINE__, "pve_mod_worker process started successfully");
}
# ============================================================================
@ -249,8 +199,6 @@ sub _pve_mod_keep_alive {
exit(0);
};
_reap_orphaned_collectors();
debug(__LINE__, "Worker starting all collectors");
_initialise_sensors_collector();
_initialise_graphics_collectors();
@ -503,74 +451,6 @@ sub _stop_child_collectors {
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
# ============================================================================

View File

@ -68,11 +68,9 @@ sub _merge_graphics_files {
my $merged = {
Graphics => {
Intel => {},
NVIDIA => {},
AMD => {},
temp_unit => $config{system_info}{temp_unit},
ignore_temp_below => $config{system_info}{ignore_temp_below} + 0,
Intel => {},
NVIDIA => {},
AMD => {},
}
};
@ -136,7 +134,7 @@ sub _load_graphics_data {
\@filepaths,
$graphics_cache,
\&_merge_graphics_files,
{ Graphics => { Intel => {}, NVIDIA => {}, AMD => {}, temp_unit => $config{system_info}{temp_unit}, ignore_temp_below => $config{system_info}{ignore_temp_below} + 0 } }
{ Graphics => { Intel => {}, NVIDIA => {}, AMD => {} } }
);
my $intel_count = scalar(keys %{$data->{Graphics}{Intel} // {}});

View File

@ -223,16 +223,15 @@ Ext.define('PVE.node.StatusView', {
} catch(e) {
objValue = {};
}
this.show();
// sensors configuration
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 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 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 cpuKeysRpi = Object.keys(objValue).filter(item => String(item).startsWith('cpu_thermal-virtual-')).sort();
const bINTEL = cpuKeysI.length > 0 ? true : false;
const INTELPackagePrefix = value.cpu_temp_target == 'Core' ? 'Core ' : 'Package id';
const INTELPackageCaption = value.cpu_temp_target == 'Core' ? 'Core' : 'Package';
const INTELPackagePrefix = 'Core' == 'Core' ? 'Core ' : 'Package id';
const INTELPackageCaption = 'Core' == 'Core' ? 'Core' : 'Package';
let AMDPackagePrefix = 'Tccd';
let AMDPackageCaption = 'CCD';
@ -306,7 +305,7 @@ Ext.define('PVE.node.StatusView', {
}
});
if (!isNaN(tempVal) && tempVal >= cpuIgnoreThreshold) {
if (!isNaN(tempVal)) {
let tempStyle = '';
if (!isNaN(tempMax) && tempVal >= tempMax) {
tempStyle = 'color: #FFC300; font-weight: bold;';
@ -393,16 +392,9 @@ Ext.define('PVE.node.StatusView', {
this.hide();
return '';
} else if (!gpuStats || !gpuStats.Graphics) {
this.hide();
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;">';
// Intel GPUs - Secondary details
@ -452,16 +444,7 @@ Ext.define('PVE.node.StatusView', {
const stats = gpuData.stats;
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
if (stats.utilization && stats.utilization.memory) {
const memUsage = parseInt(stats.utilization.memory);
@ -484,21 +467,13 @@ Ext.define('PVE.node.StatusView', {
// Temperature
if (stats.temperature) {
const gpuTemp = gpuTempHelper.getTemp(parseFloat(stats.temperature.gpu));
const gpuIgnoreThreshold = gpuTempHelper.getTemp(parseFloat(gpuStats.ignore_temp_below));
if (gpuTemp >= gpuIgnoreThreshold) {
const tempUnit = gpuTempHelper.getUnit();
// 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>`);
let tempStyle = '';
if (stats.temperature.gpu >= 80) {
tempStyle = 'color: red; font-weight: bold;';
} else if (stats.temperature.gpu >= 70) {
tempStyle = 'color: #FFC300; font-weight: bold;';
}
details.push(`Temp: <span style="${tempStyle}">${stats.temperature.gpu}${stats.temperature.unit}</span>`);
}
// Power
@ -514,12 +489,9 @@ Ext.define('PVE.node.StatusView', {
}
html += '</table>';
if (html.indexOf('<tr>') <= 0) {
this.hide();
return '';
}
this.show();
return '<div style="padding-left: 20px; box-sizing: border-box;">' + html + '</div>';
return html.indexOf('<tr>') > 0
? '<div style="padding-left: 20px; box-sizing: border-box;">' + html + '</div>'
: '';
},
},
{
@ -548,9 +520,7 @@ Ext.define('PVE.node.StatusView', {
} catch(e) {
objValue = {};
}
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 tempHelper = Ext.create('PVE.mod.TempHelper', {srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: objValue.temp_unit === 'F' ? PVE.mod.TempHelper.FAHRENHEIT : PVE.mod.TempHelper.CELSIUS});
const drvKeys = Object.keys(objValue).filter(item => String(item).startsWith(addressPrefix)).sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }));
let drvData = [];
drvKeys.forEach((drvKey) => {
@ -566,7 +536,7 @@ Ext.define('PVE.node.StatusView', {
tempCrit = tempHelper.getTemp(parseFloat(drv[sensorName][secondLevelKey]));
}
});
if (!isNaN(tempVal) && tempVal >= ignoreThreshold) {
if (!isNaN(tempVal)) {
let tempStyle = '';
if (!isNaN(tempMax) && tempVal >= tempMax) {
tempStyle = 'color: #FFC300; font-weight: bold;';
@ -630,9 +600,7 @@ Ext.define('PVE.node.StatusView', {
} catch(e) {
objValue = {};
}
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 tempHelper = Ext.create('PVE.mod.TempHelper', {srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: objValue.temp_unit === 'F' ? PVE.mod.TempHelper.FAHRENHEIT : PVE.mod.TempHelper.CELSIUS});
const nvmeKeys = Object.keys(objValue).filter(item => String(item).startsWith(addressPrefix)).sort();
let nvmeData = [];
nvmeKeys.forEach((nvmeKey, index) => {
@ -650,7 +618,7 @@ Ext.define('PVE.node.StatusView', {
model = objValue[nvmeKey]['model'] || 'Unknown';
serial = objValue[nvmeKey]['serial'] || '';
if (!isNaN(tempVal) && tempVal >= ignoreThreshold) {
if (!isNaN(tempVal)) {
let tempStyle = '';
if (!isNaN(tempMax) && tempVal >= tempMax) {
tempStyle = 'color: #FFC300; font-weight: bold;';
@ -720,9 +688,7 @@ Ext.define('PVE.node.StatusView', {
} catch(e) {
objValue = {};
}
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 tempHelper = Ext.create('PVE.mod.TempHelper', {srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: objValue.temp_unit === 'F' ? PVE.mod.TempHelper.FAHRENHEIT : PVE.mod.TempHelper.CELSIUS});
// Keep only keys that do not belong to known categories
const otherKeys = Object.keys(objValue).filter(key =>
@ -757,7 +723,7 @@ Ext.define('PVE.node.StatusView', {
}
});
if (!isNaN(tempVal) && tempVal >= ignoreThreshold) {
if (!isNaN(tempVal)) {
let tempStyle = '';
if (!isNaN(tempMax) && tempVal >= tempMax) {
tempStyle = 'color: #FFC300; font-weight: bold;';
@ -822,7 +788,6 @@ Ext.define('PVE.node.StatusView', {
} catch(e) {
objValue = {};
}
this.show();
// Recursive function to find fan keys and values
function findFanKeys(obj, fanKeys, parentKey = null) {
@ -881,7 +846,6 @@ Ext.define('PVE.node.StatusView', {
this.hide();
return '';
} else if (!gpuStats || !gpuStats.Graphics || !gpuStats.Graphics.NVIDIA) {
this.hide();
return '';
}
@ -909,307 +873,198 @@ Ext.define('PVE.node.StatusView', {
});
if (rows.length === 0) {
this.hide();
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>';
},
},
{
itemId: 'upsc',
colspan: 2,
title: gettext('UPS Status'),
iconCls: 'fa fa-fw fa-battery-three-quarters',
valueField: 'PveMod_upsInfo',
printBar: true,
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) {
itemId: 'upsc',
colspan: 2,
printBar: false,
title: gettext('UPS Status'),
iconCls: 'fa fa-fw fa-battery-three-quarters',
textField: 'PveMod_upsInfo',
renderer: function(value) {
let objValue;
try {
objValue = value || {};
} catch (e) {
} catch(e) {
objValue = {};
}
if (objValue.disabled === true) {
this.hide();
this.setPrintBar(false);
return '';
} else if (!objValue || Object.keys(objValue).length === 0) {
return 'N/A';
}
this.show();
const upsKeys = Object.keys(objValue).filter(function(k) {
return objValue[k] && typeof objValue[k] === 'object' && !Array.isArray(objValue[k]);
});
if (!upsKeys.length) {
this.hide();
this.setPrintBar(false);
return '';
// Helper function to get status color
function getStatusColor(status) {
if (!status) return '#999';
const statusUpper = status.toUpperCase();
if (statusUpper.includes('OL')) return null;
if (statusUpper.includes('OB')) return '#d9534f';
if (statusUpper.includes('LB')) return '#d9534f';
return '#f0ad4e';
}
this.show();
this.setPrintBar(true);
function formatRuntime(seconds) {
const s = parseInt(seconds, 10);
if (!s || isNaN(s)) {
// 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;
}
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
if (h > 0) {
return h + 'h ' + m + 'm';
}
return m + 'm';
}
function statusText(upsStatus) {
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' };
// Helper function to format runtime
function formatRuntime(seconds) {
if (!seconds || isNaN(seconds)) return 'N/A';
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}m ${secs}s`;
}
function colorize(label, color) {
if (!color) {
return label;
}
return '<span style="color:' + color + ';">' + label + '</span>';
}
let aboveBarText = '';
const rows = [];
upsKeys.forEach(function(upsKey) {
const upsData = objValue[upsKey] || {};
const charge = parseFloat(upsData['battery.charge']);
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']);
// Process each UPS in the data
let allDisplayItems = [];
Object.keys(objValue).forEach(upsKey => {
const upsData = objValue[upsKey];
// Extract key UPS information
const batteryCharge = upsData['battery.charge'];
const batteryRuntime = upsData['battery.runtime'];
const inputVoltage = upsData['input.voltage'];
const upsLoad = upsData['ups.load'];
const upsStatus = upsData['ups.status'];
const upsModel = upsData['ups.model'] || upsData['device.model'];
const testResult = upsData['ups.test.result'];
const manufacturingDate = upsData['battery.mfr.date'];
const batteryChargeLow = upsData['battery.charge.low'];
const batteryRuntimeLow = upsData['battery.runtime.low'];
const upsRealPowerNominal = upsData['ups.realpower.nominal'];
const batteryMfrDate = upsData['battery.mfr.date'];
// Above the bar: "Battery capacity" on the left, charge% (Runtime: ...) on the right.
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>';
// Main status line with all metrics
let statusLine = '';
// General information table: Status, Output, Input, Load, Test.
const infoBits = [];
if (st.text) {
infoBits.push('Status: ' + colorize(st.text, st.color));
}
if (!isNaN(watts)) {
infoBits.push('Output: ' + Math.round(watts) + 'W');
}
if (!isNaN(inputVoltage)) {
const places = inputVoltage >= 50 ? 0 : 1;
infoBits.push('Input: ' + inputVoltage.toFixed(places) + ' V');
}
if (!isNaN(load)) {
infoBits.push('Load: ' + Math.round(load) + '%');
}
if (manufacturingDate) {
infoBits.push('Manufacturing Date: ' + manufacturingDate);
}
if (testResult) {
infoBits.push('Test: ' + testResult);
// Status
if (upsStatus) {
const statusUpper = upsStatus.toUpperCase();
let statusText = 'Unknown';
let statusColor = '#f0ad4e';
if (statusUpper.includes('OL')) {
statusText = 'Online';
statusColor = null;
} else if (statusUpper.includes('OB')) {
statusText = 'On Battery';
statusColor = '#d9534f';
} else if (statusUpper.includes('LB')) {
statusText = 'Low Battery';
statusColor = '#d9534f';
} else {
statusText = upsStatus;
statusColor = '#f0ad4e';
}
let statusStyle = statusColor ? ('color: ' + statusColor + ';') : '';
statusLine += 'Status: <span style="' + statusStyle + '">' + statusText + '</span>';
} else {
statusLine += 'Status: <span>N/A</span>';
}
rows.push(
'<tr>' +
'<td style="padding: 2px 10px 2px 0; text-align: left; width: 30%; vertical-align: top; overflow-wrap: anywhere; word-break: break-word;">' + model + '</td>' +
'<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>' +
'</tr>'
);
// Battery charge
if (statusLine) statusLine += ' | ';
if (batteryCharge) {
const chargeColor = getPercentageColor(batteryCharge, false);
let chargeStyle = chargeColor ? ('color: ' + chargeColor + ';') : '';
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);
});
// Stash for updateValue to consume — no DOM writes here.
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>';
// Format the final output for all UPS devices
return '<div style="padding-left: 20px; box-sizing: border-box;"><table style="width: 100%; border-collapse: collapse; table-layout: fixed;">' + allDisplayItems.join('') + '</table></div>';
}
},
},
{
xtype: 'box',
colspan: 2,
@ -1277,7 +1132,6 @@ Ext.define('PVE.node.StatusView', {
this.hide();
return '';
}
this.show();
const titleMap = {
manufacturer: 'Manufacturer',

View File

@ -15,7 +15,6 @@ our @EXPORT_OK = qw(
debug
read_sysfs
is_process_alive
get_process_ppid
read_lock_pid
acquire_exclusive_lock
ensure_pve_mod_directory_exists
@ -92,33 +91,7 @@ sub read_sysfs {
sub is_process_alive {
my ($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;
return -d "/proc/$pid";
}
sub read_lock_pid {

View File

@ -17,6 +17,7 @@ enable_hdd_temp=0
enable_nvme_temp=0
enable_fan_speed=0
display_zero_speed_fans=0
temp_unit=C
[ups]
enabled=0
@ -25,8 +26,6 @@ device_name=ups@localhost
[system_info]
enabled=0
type=1
temp_unit=C
ignore_temp_below=5
# 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.

View File

@ -65,11 +65,11 @@ node_info_defaults() {
LM_SENSORS_ENABLED=0
ENABLE_CPU=0; CPU_TEMP_TARGET="Core"
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
ENABLE_FAN_SPEED=0; DISPLAY_ZERO_SPEED_FANS=0; TEMP_UNIT="C"
ENABLE_INTEL_GPU_INFO=0; ENABLE_NVIDIA_GPU_INFO=0; ENABLE_AMD_GPU_INFO=0
ENABLE_GPU_HISTORY=0
ENABLE_UPS=0; UPS_DEVICE_NAME="ups@localhost"
ENABLE_SYSTEM_INFO=0; SYSTEM_INFO_TYPE=1; TEMP_UNIT="C"; IGNORE_TEMP_BELOW=5
ENABLE_SYSTEM_INFO=0; SYSTEM_INFO_TYPE=1
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_OUTPUT_FILE="/tmp/intel-gpu-top-output.txt"
@ -104,12 +104,11 @@ node_info_load_conf() {
lm_sensors.enable_other_temp) ENABLE_OTHER_TEMP="$val" ;;
lm_sensors.enable_fan_speed) ENABLE_FAN_SPEED="$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.device_name) UPS_DEVICE_NAME="$val" ;;
system_info.enabled) ENABLE_SYSTEM_INFO="$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_output_file) DEBUG_LM_SENSORS_FILE="$val" ;;
debug.intel_mode) DEBUG_INTEL="$val" ;;
@ -134,11 +133,11 @@ node_info_configure() {
LM_SENSORS_ENABLED=0
ENABLE_CPU=0; CPU_TEMP_TARGET="Core"
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
ENABLE_FAN_SPEED=0; DISPLAY_ZERO_SPEED_FANS=0; TEMP_UNIT="C"
ENABLE_INTEL_GPU_INFO=0; ENABLE_NVIDIA_GPU_INFO=0; ENABLE_AMD_GPU_INFO=0
ENABLE_GPU_HISTORY=0
ENABLE_UPS=0; UPS_DEVICE_NAME="ups@localhost"
ENABLE_SYSTEM_INFO=0; SYSTEM_INFO_TYPE=1; TEMP_UNIT="C"; IGNORE_TEMP_BELOW=5
ENABLE_SYSTEM_INFO=0; SYSTEM_INFO_TYPE=1
local lm_sensors_ok=false
local sensors_detected=false
@ -295,26 +294,6 @@ node_info_configure() {
[fF]) TEMP_UNIT="F"; info "Using Fahrenheit." ;;
*) TEMP_UNIT="C"; info "Using Celsius." ;;
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
#endregion Temperature unit
fi
@ -499,6 +478,7 @@ enable_nvme_temp=${ENABLE_NVME_TEMP}
enable_other_temp=${ENABLE_OTHER_TEMP}
enable_fan_speed=${ENABLE_FAN_SPEED}
display_zero_speed_fans=${DISPLAY_ZERO_SPEED_FANS}
temp_unit=${TEMP_UNIT}
[ups]
enabled=${ENABLE_UPS}
@ -507,8 +487,6 @@ device_name=${UPS_DEVICE_NAME}
[system_info]
enabled=${ENABLE_SYSTEM_INFO}
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.
# Data is read from the file path instead. Useful for development/testing.