diff --git a/pve-mod-gui-sensors.sh b/pve-mod-gui-sensors.sh index c28345e..2daef36 100644 --- a/pve-mod-gui-sensors.sh +++ b/pve-mod-gui-sensors.sh @@ -38,8 +38,11 @@ JSON_EXPORT_FILENAME="sensorsdata.json" # File paths PVE_MANAGER_LIB_JS_FILE="/usr/share/pve-manager/js/pvemanagerlib.js" +PVE_MOD_JS_SOURCE_FILE="$SCRIPT_CWD/PveMod_PveNodeStatusView.js" +PVE_MOD_JS_TARGET_FILE="/usr/share/pve-manager/js/PveMod_PveNodeStatusView.js" NODES_PM_FILE="/usr/share/perl5/PVE/API2/Nodes.pm" PVE_SENSOR_INFO_MOD_FILE="/usr/share/perl5/PVE/API2/PveMod_SensorInfo.pm" +PVE_SENSOR_INFO_SOURCE_FILE="$SCRIPT_CWD/PveMod_SensorInfo.pm" #region message tools # Section header (bold) @@ -480,46 +483,9 @@ function install_mod { install_sensor_monitor_module insert_sensor_monitor_into_pve - exit - - #### Temperature helper parameters #### - msgb "\n=== Creating temperature conversion helper ===" - HELPERCTORPARAMS=$([[ "$TEMP_UNIT" = "F" ]] && \ - echo '{srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: PVE.mod.TempHelper.FAHRENHEIT}' || \ - echo '{srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: PVE.mod.TempHelper.CELSIUS}') - info "Temperature helper configured for $TEMP_UNIT." - - #### Expand StatusView space #### - expand_statusview_space - - #### Insert temperature helper #### - generate_and_insert_temp_helper - - #### Generate and insert widgets #### - msgb "\n=== Making visual adjustments ===" - - generate_and_insert_widget "$ENABLE_SYSTEM_INFO" "generate_system_info" "system_info" - generate_and_insert_widget "$ENABLE_UPS" "generate_ups_widget" "ups" - generate_and_insert_widget "$ENABLE_GPU_INFO" "generate_gpu_widget" "gpu" - generate_and_insert_widget "$ENABLE_HDD_TEMP" "generate_hdd_widget" "hdd" - generate_and_insert_widget "$ENABLE_NVME_TEMP" "generate_nvme_widget" "nvme" - - if [[ "$ENABLE_HDD_TEMP" = true || "$ENABLE_NVME_TEMP" = true ]]; then - generate_drive_header - info "Drive headers added." - fi - - generate_and_insert_widget "$ENABLE_FAN_SPEED" "generate_fan_widget" "fan" - generate_and_insert_widget "$ENABLE_RAM_TEMP" "generate_ram_widget" "ram" - generate_and_insert_widget "$ENABLE_CPU" "generate_cpu_widget" "cpu" - - #### Visual separation #### - add_visual_separator - info "Added visual separator for modified items." - - #### Node summary #### - setup_node_summary_container - info "Node summary box moved into its own container." + #### Install UI modification module #### + msgb "\n=== Installing UI modification module ===" + install_node_status_view_module msgb "\n=== Finalizing installation ===" @@ -560,14 +526,13 @@ sanitize_sensors_output() { # Install and configure the Sensor Monitor Perl module install_sensor_monitor_module() { # Check if source file exists - if [[ ! -f "$GPU_MONITOR_SOURCE_FILE" ]]; then - err "Source file not found: $GPU_MONITOR_SOURCE_FILE" + if [[ ! -f "$PVE_SENSOR_INFO_SOURCE_FILE" ]]; then + err "Source file not found: $PVE_SENSOR_INFO_SOURCE_FILE" fi # Copy the module file - # todo - how to install module??!?! - #cp "$GPU_MONITOR_SOURCE_FILE" "$PVE_SENSOR_INFO_MOD_FILE" || err "Failed to copy $GPU_MONITOR_SOURCE_FILE to $PVE_SENSOR_INFO_MOD_FILE" - info "Copied GPU Monitor module to $PVE_SENSOR_INFO_MOD_FILE" + cp "$PVE_SENSOR_INFO_SOURCE_FILE" "$PVE_SENSOR_INFO_MOD_FILE" || err "Failed to copy $PVE_SENSOR_INFO_SOURCE_FILE to $PVE_SENSOR_INFO_MOD_FILE" + info "Copied Sensor Monitor module to $PVE_SENSOR_INFO_MOD_FILE" # Convert boolean flags to Perl format (1 or 0) local intel_enabled=$([[ "$ENABLE_INTEL_GPU_INFO" = true ]] && echo 1 || echo 0) @@ -606,10 +571,10 @@ install_sensor_monitor_module() { insert_sensor_monitor_into_pve() { #region PveSensorInfoMod heredoc sed -i '/my \$dinfo = df('\''\/'\'', 1);/i\ - # Collect sensor data from PveSensorInfoMod\ + # Collect sensor data from PveMod_SensorInfo\ # Bad practice to add use here, but cleaner implementation would require several extensive modifications.\ - use PVE::API2::GPUMonitor;\ - $res->{sensorsJSONOutput} = PVE::API2::GPUMonitor::get_sensors_stats();\ + use PVE::API2::PveMod_SensorInfo;\ + $res->{sensorsJSONOutput} = PVE::API2::PveMod_SensorInfo::get_sensors_stats();\ ' "$NODES_PM_FILE" #endregion PveSensorInfoMod heredoc info "Sensor data retriever added to \"$NODES_PM_FILE\"." @@ -641,1072 +606,47 @@ collect_system_info() { } #endregion node info insertion -#region widget generation functions -# Helper function to insert widget after thermal items -insert_widget_after_thermal() { - local widget_file="$1" - sed -i "/^Ext.define('PVE.node.StatusView',/ { - :a - /items:/!{N;ba;} - :b - /'cpus.*},/!{N;bb;} - r $widget_file - }" "$PVE_MANAGER_LIB_JS_FILE" -} - -# Helper function to generate widget and insert it -generate_and_insert_widget() { - local enable_flag="$1" - local generator_func="$2" - local widget_name="$3" - - if [ "$enable_flag" = true ]; then - local temp_js_file="/tmp/${widget_name}_widget.js" - "$generator_func" "$temp_js_file" - insert_widget_after_thermal "$temp_js_file" - rm "$temp_js_file" - info "Inserted $widget_name widget." - fi -} - -# Function to generate drive header -generate_drive_header() { - if [ "$ENABLE_NVME_TEMP" = true ] || [ "$ENABLE_HDD_TEMP" = true ]; then - local temp_js_file="/tmp/drive_header.js" - #region drive header heredoc - cat > "$temp_js_file" <<'EOF' - { - xtype: 'box', - colspan: 2, - html: gettext('Drive(s)'), - }, -EOF -#endregion drive header heredoc - if [[ $? -ne 0 ]]; then - echo "Error: Failed to generate drive header code" >&2 - exit 1 - fi - - insert_widget_after_thermal "$temp_js_file" - rm "$temp_js_file" +#region UI Module Installation +# Install the UI modification module +install_node_status_view_module() { + # Check if source file exists + if [[ ! -f "$PVE_MOD_JS_SOURCE_FILE" ]]; then + err "Source file not found: $PVE_MOD_JS_SOURCE_FILE" fi -} -# Function to expand space and modify StatusView properties -expand_statusview_space() { - msgb "\n=== Expanding StatusView space ===" + # Copy the JavaScript module to PVE manager directory + cp "$PVE_MOD_JS_SOURCE_FILE" "$PVE_MOD_JS_TARGET_FILE" || err "Failed to copy $PVE_MOD_JS_SOURCE_FILE to $PVE_MOD_JS_TARGET_FILE" + info "Copied UI module to $PVE_MOD_JS_TARGET_FILE" - # Apply multiple modifications to the StatusView definition - sed -i "/Ext.define('PVE\.node\.StatusView'/,/\},/ { - s/\(bodyPadding:\) '[^']*'/\1 '20 15 20 15'/ - s/height: [0-9]\+/minHeight: 360,\n\tflex: 1,\n\tcollapsible: true,\n\ttitleCollapse: true/ - s/\(tableAttrs:.*$\)/trAttrs: \{ valign: 'top' \},\n\t\1/ - }" "$PVE_MANAGER_LIB_JS_FILE" - - if [[ $? -ne 0 ]]; then - echo "Error: Failed to expand StatusView space" >&2 - exit 1 - fi - - info "Expanded space in \"$PVE_MANAGER_LIB_JS_FILE\"." -} - -# Function to move node summary into its own container -setup_node_summary_container() { - # Move the node summary box into its own container - local temp_js_file="/tmp/summary_container.js" - #region summary container heredoc - cat > "$temp_js_file" <<'EOF' -{ - xtype: 'container', - itemId: 'summarycontainer', - layout: 'column', - minWidth: 700, - defaults: { - minHeight: 350, - padding: 5, - columnWidth: 1, - }, - items: [ - nodeStatus, - ] -}, -EOF -#endregion summary container heredoc - if [[ $? -ne 0 ]]; then - echo "Error: Failed to generate summary container code" >&2 - exit 1 - fi - - # Insert the new container after finding the nodeStatus and items pattern - sed -i "/^\s*nodeStatus: nodeStatus,/ { - :a - /items: \[/ !{N;ba;} - r $temp_js_file - }" "$PVE_MANAGER_LIB_JS_FILE" - - rm "$temp_js_file" - - # Deactivate the original box instance - sed -i "/^\s*nodeStatus: nodeStatus,/ { - :a - /itemId: 'itemcontainer',/ !{N;ba;} - n; - :b - /nodeStatus,/ !{N;bb;} - s/nodeStatus/\/\/nodeStatus/ - }" "$PVE_MANAGER_LIB_JS_FILE" - - if [[ $? -ne 0 ]]; then - echo "Error: Failed to deactivate original nodeStatus instance" >&2 - exit 1 - fi -} - -# Function to add visual spacing separator after the last widget -add_visual_separator() { - # Check for the presence of items in the reverse order of display - local lastItemId="" - - if [ "$ENABLE_UPS" = true ]; then - lastItemId="upsc" - elif [ "$ENABLE_GPU" = true ]; then - lastItemId="gpuInfo" - elif [ "$ENABLE_HDD_TEMP" = true ]; then - lastItemId="thermalHdd" - elif [ "$ENABLE_NVME_TEMP" = true ]; then - lastItemId="thermalNvme" - elif [ "$ENABLE_FAN_SPEED" = true ]; then - lastItemId="speedFan" + # Comment out the original PVE.node.StatusView definition in pvemanagerlib.js + # This allows our custom module to provide the new definition + if grep -q "^Ext.define('PVE.node.StatusView'," "$PVE_MANAGER_LIB_JS_FILE" 2>/dev/null; then + # Find the start of the definition and comment it out until the matching closing brace + sed -i "/^Ext\.define('PVE\.node\.StatusView',/,/^});/s|^|// |" "$PVE_MANAGER_LIB_JS_FILE" + info "Commented out original StatusView definition in pvemanagerlib.js" else - lastItemId="thermalCpu" + warn "Original StatusView definition not found in expected format in pvemanagerlib.js" fi - if [ -n "$lastItemId" ]; then - local temp_js_file="/tmp/visual_separator.js" - - #region visual spacing heredoc - cat > "$temp_js_file" <<'EOF' - { - xtype: 'box', - colspan: 2, - padding: '0 0 20 0', - }, -EOF -#endregion visual spacing heredoc - if [[ $? -ne 0 ]]; then - echo "Error: Failed to generate visual separator code" >&2 - exit 1 - fi - - # Insert after the specific lastItemId (different pattern than thermal) - sed -i "/^Ext.define('PVE.node.StatusView',/ { - :a; - /^.*{.*'$lastItemId'.*},/!{N;ba;} - r $temp_js_file - }" "$PVE_MANAGER_LIB_JS_FILE" - - rm "$temp_js_file" + # Add a dynamic script loader to load our custom module + # Insert before the commented-out Ext.define so it loads early + if ! grep -q "PveMod_PveNodeStatusView.js" "$PVE_MANAGER_LIB_JS_FILE" 2>/dev/null; then + # Use ExtJS Loader to dynamically load our custom module + sed -i "/^\/\/ Ext\.define('PVE\.node\.StatusView',/i\\ +// Load custom PVE.node.StatusView from external module\\ +Ext.Loader.loadScript({\\ + url: '/pve2/js/PveMod_PveNodeStatusView.js',\\ + onLoad: function() { console.log('Loaded PveMod_PveNodeStatusView.js'); },\\ + onError: function() { console.error('Failed to load PveMod_PveNodeStatusView.js'); }\\ +});\\ +" "$PVE_MANAGER_LIB_JS_FILE" + info "Added dynamic loader for custom UI module in pvemanagerlib.js" + else + info "Custom UI module loader already present in pvemanagerlib.js" fi } +#endregion UI Module Installation -# Function to generate system info widget -generate_system_info() { - #region system info heredoc - cat > "$1" <<'EOF' - { - itemId: 'sysinfo', - colspan: 2, - printBar: false, - title: gettext('System Information'), - textField: 'systemInfo', - renderer: function(value){ - return value; - } - }, -EOF - #endregion system info heredoc - if [[ $? -ne 0 ]]; then - echo "Error: Failed to generate system info code" >&2 - exit 1 - fi -} - -# Function to generate and insert temperature conversion helper class -generate_and_insert_temp_helper() { - local temp_js_file="/tmp/temp_helper.js" - - msgb "\n=== Inserting temperature helper ===" - - #region temp helper heredoc - cat > "$temp_js_file" <<'EOF' -Ext.define('PVE.mod.TempHelper', { - //singleton: true, - - requires: ['Ext.util.Format'], - - statics: { - CELSIUS: 0, - FAHRENHEIT: 1 - }, - - srcUnit: null, - dstUnit: null, - - isValidUnit: function (unit) { - return ( - Ext.isNumber(unit) && (unit === this.self.CELSIUS || unit === this.self.FAHRENHEIT) - ); - }, - - constructor: function (config) { - this.srcUnit = config && this.isValidUnit(config.srcUnit) ? config.srcUnit : this.self.CELSIUS; - this.dstUnit = config && this.isValidUnit(config.dstUnit) ? config.dstUnit : this.self.CELSIUS; - }, - - toFahrenheit: function (tempCelsius) { - return Ext.isNumber(tempCelsius) - ? tempCelsius * 9 / 5 + 32 - : NaN; - }, - - toCelsius: function (tempFahrenheit) { - return Ext.isNumber(tempFahrenheit) - ? (tempFahrenheit - 32) * 5 / 9 - : NaN; - }, - - getTemp: function (value) { - if (this.srcUnit !== this.dstUnit) { - switch (this.srcUnit) { - case this.self.CELSIUS: - switch (this.dstUnit) { - case this.self.FAHRENHEIT: - return this.toFahrenheit(value); - - default: - Ext.raise({ - msg: - 'Unsupported destination temperature unit: ' + this.dstUnit, - }); - } - case this.self.FAHRENHEIT: - switch (this.dstUnit) { - case this.self.CELSIUS: - return this.toCelsius(value); - - default: - Ext.raise({ - msg: - 'Unsupported destination temperature unit: ' + this.dstUnit, - }); - } - default: - Ext.raise({ - msg: 'Unsupported source temperature unit: ' + this.srcUnit, - }); - } - } else { - return value; - } - }, - - getUnit: function(plainText) { - switch (this.dstUnit) { - case this.self.CELSIUS: - return plainText !== true ? '°C' : '\'C'; - - case this.self.FAHRENHEIT: - return plainText !== true ? '°F' : '\'F'; - - default: - Ext.raise({ - msg: 'Unsupported destination temperature unit: ' + this.srcUnit, - }); - } - }, -}); -EOF - #endregion temp helper heredoc - if [[ $? -ne 0 ]]; then - echo "Error: Failed to generate temp helper code" >&2 - exit 1 - fi - - sed -i "/^Ext.define('PVE.node.StatusView'/e cat /tmp/temp_helper.js" "$PVE_MANAGER_LIB_JS_FILE" - rm "$temp_js_file" - - info "Temperature helper inserted successfully." -} - -# Function to generate CPU widget -generate_cpu_widget() { - #region cpu widget heredoc - # use subshell to allow variable expansion - ( - export CPU_ITEMS_PER_ROW - export CPU_TEMP_TARGET - export HELPERCTORPARAMS - - cat <<'EOF' | envsubst '$CPU_ITEMS_PER_ROW $CPU_TEMP_TARGET $HELPERCTORPARAMS' > "$1" - { - itemId: 'thermalCpu', - colspan: 2, - printBar: false, - title: gettext('CPU Thermal State'), - iconCls: 'fa fa-fw fa-thermometer-half', - textField: 'sensorsOutput', - renderer: function(value){ - // sensors configuration - const cpuTempHelper = Ext.create('PVE.mod.TempHelper', $HELPERCTORPARAMS); - // display configuration - const itemsPerRow = $CPU_ITEMS_PER_ROW; - // --- - let objValue; - try { - objValue = JSON.parse(value) || {}; - } catch(e) { - objValue = {}; - } - 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 bINTEL = cpuKeysI.length > 0 ? true : false; - const INTELPackagePrefix = '$CPU_TEMP_TARGET' == 'Core' ? 'Core ' : 'Package id'; - const INTELPackageCaption = '$CPU_TEMP_TARGET' == 'Core' ? 'Core' : 'Package'; - let AMDPackagePrefix = 'Tccd'; - let AMDPackageCaption = 'CCD'; - - if (cpuKeysA.length > 0) { - let bTccd = false; - let bTctl = false; - let bTdie = false; - let bCpuCoreTemp = false; - cpuKeysA.forEach((cpuKey, cpuIndex) => { - let items = objValue[cpuKey]; - bTccd = Object.keys(items).findIndex(item => { return String(item).startsWith('Tccd'); }) >= 0; - bTctl = Object.keys(items).findIndex(item => { return String(item).startsWith('Tctl'); }) >= 0; - bTdie = Object.keys(items).findIndex(item => { return String(item).startsWith('Tdie'); }) >= 0; - bCpuCoreTemp = Object.keys(items).findIndex(item => { return String(item) === 'CPU Core Temp'; }) >= 0; - }); - if (bTccd && '$CPU_TEMP_TARGET' == 'Core') { - AMDPackagePrefix = 'Tccd'; - AMDPackageCaption = 'ccd'; - } else if (bCpuCoreTemp && '$CPU_TEMP_TARGET' == 'Package') { - AMDPackagePrefix = 'CPU Core Temp'; - AMDPackageCaption = 'CPU Core Temp'; - } else if (bTdie) { - AMDPackagePrefix = 'Tdie'; - AMDPackageCaption = 'die'; - } else if (bTctl) { - AMDPackagePrefix = 'Tctl'; - AMDPackageCaption = 'ctl'; - } else { - AMDPackagePrefix = 'temp'; - AMDPackageCaption = 'Temp'; - } - } - - const cpuKeys = bINTEL ? cpuKeysI : cpuKeysA; - const cpuItemPrefix = bINTEL ? INTELPackagePrefix : AMDPackagePrefix; - const cpuTempCaption = bINTEL ? INTELPackageCaption : AMDPackageCaption; - const formatTemp = bINTEL ? '0' : '0.0'; - const cpuCount = cpuKeys.length; - let temps = []; - - cpuKeys.forEach((cpuKey, cpuIndex) => { - let cpuTemps = []; - const items = objValue[cpuKey]; - const itemKeys = Object.keys(items).filter(item => { - if ('$CPU_TEMP_TARGET' == 'Core') { - // In Core mode: only show individual cores/CCDs, exclude overall CPU temp - return String(item).includes(cpuItemPrefix) || String(item).startsWith('Tccd'); - } else { - // In Package mode: show overall CPU temp and package-level readings - return String(item).includes(cpuItemPrefix) || String(item) === 'CPU Core Temp'; - } - }); - - itemKeys.forEach((coreKey) => { - try { - let tempVal = NaN, tempMax = NaN, tempCrit = NaN; - Object.keys(items[coreKey]).forEach((secondLevelKey) => { - if (secondLevelKey.endsWith('_input')) { - tempVal = cpuTempHelper.getTemp(parseFloat(items[coreKey][secondLevelKey])); - } else if (secondLevelKey.endsWith('_max')) { - tempMax = cpuTempHelper.getTemp(parseFloat(items[coreKey][secondLevelKey])); - } else if (secondLevelKey.endsWith('_crit')) { - tempCrit = cpuTempHelper.getTemp(parseFloat(items[coreKey][secondLevelKey])); - } - }); - - if (!isNaN(tempVal)) { - let tempStyle = ''; - if (!isNaN(tempMax) && tempVal >= tempMax) { - tempStyle = 'color: #FFC300; font-weight: bold;'; - } - if (!isNaN(tempCrit) && tempVal >= tempCrit) { - tempStyle = 'color: red; font-weight: bold;'; - } - - let tempStr = ''; - - // Enhanced parsing for AMD temperatures - if (coreKey.startsWith('Tccd')) { - let tempIndex = coreKey.match(/Tccd(\d+)/); - if (tempIndex !== null && tempIndex.length > 1) { - tempIndex = tempIndex[1]; - tempStr = `${cpuTempCaption} ${tempIndex}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()}`; - } else { - tempStr = `${cpuTempCaption}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()}`; - } - } - // Handle CPU Core Temp (single overall temperature) - else if (coreKey === 'CPU Core Temp') { - tempStr = `${cpuTempCaption}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()}`; - } - // Enhanced parsing for Intel cores (P-Core, E-Core, regular Core) - else { - let tempIndex = coreKey.match(/(?:P\s+Core|E\s+Core|Core)\s*(\d+)/); - if (tempIndex !== null && tempIndex.length > 1) { - tempIndex = tempIndex[1]; - let coreType = coreKey.startsWith('P Core') ? 'P Core' : - coreKey.startsWith('E Core') ? 'E Core' : - cpuTempCaption; - tempStr = `${coreType} ${tempIndex}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()}`; - } else { - // fallback for CPUs which do not have a core index - let coreType = coreKey.startsWith('P Core') ? 'P Core' : - coreKey.startsWith('E Core') ? 'E Core' : - cpuTempCaption; - tempStr = `${coreType}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()}`; - } - } - - cpuTemps.push(tempStr); - } - } catch (e) { /*_*/ } - }); - - if(cpuTemps.length > 0) { - temps.push(cpuTemps); - } - }); - - let result = ''; - temps.forEach((cpuTemps, cpuIndex) => { - const strCoreTemps = cpuTemps.map((strTemp, index, arr) => { - return strTemp + (index + 1 < arr.length ? (itemsPerRow > 0 && (index + 1) % itemsPerRow === 0 ? '
' : ' | ') : ''); - }) - if(strCoreTemps.length > 0) { - result += (cpuCount > 1 ? `CPU ${cpuIndex+1}: ` : '') + strCoreTemps.join('') + (cpuIndex < cpuCount ? '
' : ''); - } - }); - - return '
' + (result.length > 0 ? result : 'N/A') + '
'; - } - }, -EOF - ) - #endregion cpu widget heredoc - if [[ $? -ne 0 ]]; then - echo "Error: Failed to generate cpu widget code" >&2 - exit 1 - fi -} - -# Function to generate nvme widget -generate_nvme_widget() { - #region nvme widget heredoc - # use subshell to allow variable expansion - ( - export HELPERCTORPARAMS - export NVME_ITEMS_PER_ROW - cat <<'EOF' | envsubst '$HELPERCTORPARAMS $NVME_ITEMS_PER_ROW' > "$1" - { - itemId: 'thermalNvme', - colspan: 2, - printBar: false, - title: gettext('NVMe Thermal State'), - iconCls: 'fa fa-fw fa-thermometer-half', - textField: 'sensorsOutput', - renderer: function(value) { - // sensors configuration - const addressPrefix = "nvme-pci-"; - const sensorName = "Composite"; - const tempHelper = Ext.create('PVE.mod.TempHelper', $HELPERCTORPARAMS); - // display configuration - const itemsPerRow = $NVME_ITEMS_PER_ROW; - // --- - let objValue; - try { - objValue = JSON.parse(value) || {}; - } catch(e) { - objValue = {}; - } - const nvmeKeys = Object.keys(objValue).filter(item => String(item).startsWith(addressPrefix)).sort(); - let temps = []; - nvmeKeys.forEach((nvmeKey, index) => { - try { - let tempVal = NaN, tempMax = NaN, tempCrit = NaN; - Object.keys(objValue[nvmeKey][sensorName]).forEach((secondLevelKey) => { - if (secondLevelKey.endsWith('_input')) { - tempVal = tempHelper.getTemp(parseFloat(objValue[nvmeKey][sensorName][secondLevelKey])); - } else if (secondLevelKey.endsWith('_max')) { - tempMax = tempHelper.getTemp(parseFloat(objValue[nvmeKey][sensorName][secondLevelKey])); - } else if (secondLevelKey.endsWith('_crit')) { - tempCrit = tempHelper.getTemp(parseFloat(objValue[nvmeKey][sensorName][secondLevelKey])); - } - }); - if (!isNaN(tempVal)) { - let tempStyle = ''; - if (!isNaN(tempMax) && tempVal >= tempMax) { - tempStyle = 'color: #FFC300; font-weight: bold;'; - } - if (!isNaN(tempCrit) && tempVal >= tempCrit) { - tempStyle = 'color: red; font-weight: bold;'; - } - const tempStr = `Drive ${index + 1}: ${Ext.util.Format.number(tempVal, '0.0')}${tempHelper.getUnit()}`; - temps.push(tempStr); - } - } catch(e) { /*_*/ } - }); - const result = temps.map((strTemp, index, arr) => { return strTemp + (index + 1 < arr.length ? ((index + 1) % itemsPerRow === 0 ? '
' : ' | ') : ''); }); - return '
' + (result.length > 0 ? result.join('') : 'N/A') + '
'; - } - }, -EOF - ) - #endregion nvme widget heredoc - if [[ $? -ne 0 ]]; then - echo "Error: Failed to generate nvme widget code" >&2 - exit 1 - fi -} - -# Function to generate Fan widget -generate_fan_widget() { - #region fan widget heredoc - # use subshell to allow variable expansion - ( - export DISPLAY_ZERO_SPEED_FANS - cat <<'EOF' | envsubst '$DISPLAY_ZERO_SPEED_FANS' > "$1" - { - xtype: 'box', - colspan: 2, - html: gettext('Cooling'), - }, - { - itemId: 'speedFan', - colspan: 2, - printBar: false, - title: gettext('Fan Speed(s)'), - iconCls: 'fa fa-fw fa-snowflake-o', - textField: 'sensorsOutput', - renderer: function(value) { - // --- - let objValue; - try { - objValue = JSON.parse(value) || {}; - } catch(e) { - objValue = {}; - } - - // Recursive function to find fan keys and values - function findFanKeys(obj, fanKeys, parentKey = null) { - Object.keys(obj).forEach(key => { - const value = obj[key]; - if (typeof value === 'object' && value !== null) { - // If the value is an object, recursively call the function - findFanKeys(value, fanKeys, key); - } else if (/^fan[0-9]+(_input)?$/.test(key)) { - if ($DISPLAY_ZERO_SPEED_FANS != true && value === 0) { - // Skip this fan if DISPLAY_ZERO_SPEED_FANS is false and value is 0 - return; - } - // If the key matches the pattern, add the parent key and value to the fanKeys array - fanKeys.push({ key: parentKey, value: value }); - } - }); - } - - let speeds = []; - // Loop through the parent keys - Object.keys(objValue).forEach(parentKey => { - const parentObj = objValue[parentKey]; - // Array to store fan keys and values - const fanKeys = []; - // Call the recursive function to find fan keys and values - findFanKeys(parentObj, fanKeys); - // Sort the fan keys - fanKeys.sort(); - // Process each fan key and value - fanKeys.forEach(({ key: fanKey, value: fanSpeed }) => { - try { - const fan = fanKey.charAt(0).toUpperCase() + fanKey.slice(1); // Capitalize the first letter of fanKey - speeds.push(`${fan}: ${fanSpeed} RPM`); - } catch(e) { - console.error(`Error retrieving fan speed for ${fanKey} in ${parentKey}:`, e); // Debug: Log specific error - } - }); - }); - return '
' + (speeds.length > 0 ? speeds.join(' | ') : 'N/A') + '
'; - } - }, -EOF - ) - #endregion fan widget heredoc - if [[ $? -ne 0 ]]; then - echo "Error: Failed to generate fan widget code" >&2 - exit 1 - fi -} - -# Function to generate UPS widget -generate_hdd_widget() { - #region hdd widget heredoc - # use subshell to allow variable expansion - ( - export HELPERCTORPARAMS - export HDD_ITEMS_PER_ROW - cat <<'EOF' | envsubst '$HDD_ITEMS_PER_ROW $HELPERCTORPARAMS' > "$1" - { - itemId: 'thermalHdd', - colspan: 2, - printBar: false, - title: gettext('HDD/SSD Thermal State'), - iconCls: 'fa fa-fw fa-thermometer-half', - textField: 'sensorsOutput', - renderer: function(value) { - // sensors configuration - const addressPrefix = "drivetemp-scsi-"; - const sensorName = "temp1"; - const tempHelper = Ext.create('PVE.mod.TempHelper', $HELPERCTORPARAMS); - // display configuration - const itemsPerRow = $HDD_ITEMS_PER_ROW; - // --- - let objValue; - try { - objValue = JSON.parse(value) || {}; - } catch(e) { - objValue = {}; - } - const drvKeys = Object.keys(objValue).filter(item => String(item).startsWith(addressPrefix)).sort(); - let temps = []; - drvKeys.forEach((drvKey, index) => { - try { - let tempVal = NaN, tempMax = NaN, tempCrit = NaN; - Object.keys(objValue[drvKey][sensorName]).forEach((secondLevelKey) => { - if (secondLevelKey.endsWith('_input')) { - tempVal = tempHelper.getTemp(parseFloat(objValue[drvKey][sensorName][secondLevelKey])); - } else if (secondLevelKey.endsWith('_max')) { - tempMax = tempHelper.getTemp(parseFloat(objValue[drvKey][sensorName][secondLevelKey])); - } else if (secondLevelKey.endsWith('_crit')) { - tempCrit = tempHelper.getTemp(parseFloat(objValue[drvKey][sensorName][secondLevelKey])); - } - }); - if (!isNaN(tempVal)) { - let tempStyle = ''; - if (!isNaN(tempMax) && tempVal >= tempMax) { - tempStyle = 'color: #FFC300; font-weight: bold;'; - } - if (!isNaN(tempCrit) && tempVal >= tempCrit) { - tempStyle = 'color: red; font-weight: bold;'; - } - const tempStr = `Drive ${index + 1}: ${Ext.util.Format.number(tempVal, '0.0')}${tempHelper.getUnit()}`; - temps.push(tempStr); - } - } catch(e) { /*_*/ } - }); - const result = temps.map((strTemp, index, arr) => { return strTemp + (index + 1 < arr.length ? ((index + 1) % itemsPerRow === 0 ? '
' : ' | ') : ''); }); - return '
' + (result.length > 0 ? result.join('') : 'N/A') + '
'; - } - }, -EOF - ) - #endregion hdd widget heredoc - if [[ $? -ne 0 ]]; then - echo "Error: Failed to generate hhd widget code" >&2 - exit 1 - fi -} - -# Function to generate RAM widget -generate_ram_widget() { - #region ram widget heredoc - # use subshell to allow variable expansion - ( - export HELPERCTORPARAMS - cat <<'EOF' | envsubst '$HELPERCTORPARAMS' > "$1" - { - xtype: 'box', - colspan: 2, - html: gettext('RAM'), - }, - { - itemId: 'thermalRam', - colspan: 2, - printBar: false, - title: gettext('Thermal State'), - iconCls: 'fa fa-fw fa-thermometer-half', - textField: 'sensorsOutput', - renderer: function(value) { - const cpuTempHelper = Ext.create('PVE.mod.TempHelper', $HELPERCTORPARAMS); - - let objValue; - try { - objValue = JSON.parse(value) || {}; - } catch(e) { - objValue = {}; - } - - // Recursive function to find ram keys and values - function findRamKeys(obj, ramKeys, parentKey = null) { - Object.keys(obj).forEach(key => { - const value = obj[key]; - if (typeof value === 'object' && value !== null) { - // If the value is an object, recursively call the function - findRamKeys(value, ramKeys, key); - } else if (/^temp\d+_input$/.test(key) && parentKey && parentKey.startsWith("SODIMM")) { - if (value !== 0) { - ramKeys.push({ key: parentKey, value: value}); - } - } - }); - } - - let ramTemps = []; - // Loop through the parent keys - Object.keys(objValue).forEach(parentKey => { - const parentObj = objValue[parentKey]; - // Array to store ram keys and values - const ramKeys = []; - // Call the recursive function to find ram keys and values - findRamKeys(parentObj, ramKeys); - // Sort the ramKeys keys - ramKeys.sort(); - // Process each ram key and value - ramKeys.forEach(({ key: ramKey, value: ramTemp }) => { - try { - ramTemps.push(`${ramKey}: ${ramTemp}${cpuTempHelper.getUnit()}`); - } catch(e) { - console.error(`Error retrieving Ram Temp for ${ramTemps} in ${parentKey}:`, e); // Debug: Log specific error - } - }); - }); - return '
' + (ramTemps.length > 0 ? ramTemps.join(' | ') : 'N/A') + '
'; - } - }, -EOF - ) - #endregion ram widget heredoc - if [[ $? -ne 0 ]]; then - echo "Error: Failed to generate ram widget code" >&2 - exit 1 - fi -} - -# Function to generate UPS widget -generate_ups_widget() { - #region UPS widget heredoc - cat > "$1" <<'EOF' - { - xtype: 'box', - colspan: 2, - html: gettext('UPS'), - }, - { - itemId: 'upsc', - colspan: 2, - printBar: false, - title: gettext('Device'), - iconCls: 'fa fa-fw fa-battery-three-quarters', - textField: 'upsc', - renderer: function(value) { - let objValue = {}; - try { - // Parse the UPS data - if (typeof value === 'string') { - const lines = value.split('\n'); - lines.forEach(line => { - const colonIndex = line.indexOf(':'); - if (colonIndex > 0) { - const key = line.substring(0, colonIndex).trim(); - const val = line.substring(colonIndex + 1).trim(); - objValue[key] = val; - } - }); - } else if (typeof value === 'object') { - objValue = value || {}; - } - } catch(e) { - objValue = {}; - } - - // If objValue is null or empty, return N/A - if (!objValue || Object.keys(objValue).length === 0) { - return '
N/A
'; - } - - // Helper function to get status color - // Returns a CSS color string for non-default states, or null for default (no inline color) - function getStatusColor(status) { - if (!status) return '#999'; - const statusUpper = status.toUpperCase(); - if (statusUpper.includes('OL')) return null; // default (no explicit color) - if (statusUpper.includes('OB')) return '#d9534f'; // Red for on battery - if (statusUpper.includes('LB')) return '#d9534f'; // Red for low battery - return '#f0ad4e'; // Orange for other states - } - - // Helper function to get load/charge color - // Returns null for default/good values so no inline style is emitted - function getPercentageColor(value, isLoad = false) { - if (!value || isNaN(value)) return '#999'; - const num = parseFloat(value); - if (isLoad) { - if (num >= 80) return '#d9534f'; // Red for high load - if (num >= 60) return '#f0ad4e'; // Orange for medium load - return null; // default (no explicit color) - } else { - // For battery charge - if (num <= 20) return '#d9534f'; // Red for low charge - if (num <= 50) return '#f0ad4e'; // Orange for medium charge - return null; // default (no explicit color) - } - } - - // 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`; - } - - // Extract key UPS information - const batteryCharge = objValue['battery.charge']; - const batteryRuntime = objValue['battery.runtime']; - const inputVoltage = objValue['input.voltage']; - const upsLoad = objValue['ups.load']; - const upsStatus = objValue['ups.status']; - const upsModel = objValue['ups.model'] || objValue['device.model']; - const testResult = objValue['ups.test.result']; - const batteryChargeLow = objValue['battery.charge.low']; - const batteryRuntimeLow = objValue['battery.runtime.low']; - const upsRealPowerNominal = objValue['ups.realpower.nominal']; - const batteryMfrDate = objValue['battery.mfr.date']; - - // Build the status display - let displayItems = []; - - // First line: Model info (no explicit color for default) - let modelLine = ''; - if (upsModel) { - modelLine = `${upsModel}`; - } else { - modelLine = `N/A`; - } - displayItems.push(modelLine); - - // Main status line with all metrics - let statusLine = ''; - - // Status - if (upsStatus) { - const statusUpper = upsStatus.toUpperCase(); - let statusText = 'Unknown'; - let statusColor = '#f0ad4e'; - - if (statusUpper.includes('OL')) { - statusText = 'Online'; - statusColor = null; // default (no explicit color) - } else if (statusUpper.includes('OB')) { - statusText = 'On Battery'; - statusColor = '#d9534f'; // Red for on battery - } else if (statusUpper.includes('LB')) { - statusText = 'Low Battery'; - statusColor = '#d9534f'; // Red for low battery - } else { - statusText = upsStatus; - statusColor = '#f0ad4e'; // Orange for unknown status - } - - let statusStyle = statusColor ? ('color: ' + statusColor + ';') : ''; - statusLine += 'Status: ' + statusText + ''; - } else { - statusLine += 'Status: N/A'; - } - - // Battery charge - if (statusLine) statusLine += ' | '; - if (batteryCharge) { - const chargeColor = getPercentageColor(batteryCharge, false); - let chargeStyle = chargeColor ? ('color: ' + chargeColor + ';') : ''; - statusLine += 'Battery: ' + batteryCharge + '%'; - } else { - statusLine += 'Battery: N/A'; - } - - // Load percentage - if (statusLine) statusLine += ' | '; - if (upsLoad) { - const loadColor = getPercentageColor(upsLoad, true); - let loadStyle = loadColor ? ('color: ' + loadColor + ';') : ''; - statusLine += 'Load: ' + upsLoad + '%'; - } else { - statusLine += 'Load: N/A'; - } - - // 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'; // Red if less than half of low threshold - else if (runtime <= runtimeLowThreshold) runtimeColor = '#f0ad4e'; // Orange if at low threshold - let runtimeStyle = runtimeColor ? ('color: ' + runtimeColor + ';') : ''; - statusLine += 'Runtime: ' + formatRuntime(runtime) + ''; - } else { - statusLine += 'Runtime: N/A'; - } - - // Input voltage - if (statusLine) statusLine += ' | '; - if (inputVoltage) { - statusLine += 'Input: ' + parseFloat(inputVoltage).toFixed(0) + 'V'; - } else { - statusLine += 'Input: N/A'; - } - - // 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: ' + actualWattage + 'W'; - } else { - statusLine += 'Output: N/A'; - } - - displayItems.push(statusLine); - - // Combined battery and test line - let batteryTestLine = ''; - if (batteryMfrDate) { - batteryTestLine += 'Battery MFD: ' + batteryMfrDate + ''; - } else { - batteryTestLine += 'Battery MFD: N/A'; - } - - if (testResult && !testResult.toLowerCase().includes('no test')) { - const testColor = testResult.toLowerCase().includes('passed') ? null : '#d9534f'; - let testStyle = testColor ? ('color: ' + testColor + ';') : ''; - batteryTestLine += ' | Test: ' + testResult + ''; - } else { - batteryTestLine += ' | Test: N/A'; - } - - displayItems.push(batteryTestLine); - - // Format the final output - return '
' + displayItems.join('
') + '
'; - } - }, -EOF - #endregion UPS widget heredoc - if [[ $? -ne 0 ]]; then - echo "Error: Failed to generate UPS widget code" >&2 - exit 1 - fi -} - -# Function to generate GPU widget -generate_gpu_widget() { - #region gpu widget heredoc - # use subshell to allow variable expansion - ( - export CPU_ITEMS_PER_ROW - export CPU_TEMP_TARGET - export HELPERCTORPARAMS - - cat <<'EOF' | envsubst '$CPU_ITEMS_PER_ROW $CPU_TEMP_TARGET $HELPERCTORPARAMS' > "$1" - { - itemId: 'gpu', - colspan: 2, - iconCls: 'fa fa-desktop', - title: gettext('GPU(s)'), - printBar: false, - textField: 'gpuStats', - renderer: function(gpuStats) { - console.log(gpuStats); - if (!gpuStats || !gpuStats.Graphics || !gpuStats.Graphics.Intel) { - return 'N/A'; - } - - let html = ''; - - Object.keys(gpuStats.Graphics.Intel).forEach(key => { - const gpuData = gpuStats.Graphics.Intel[key]; - console.log("here1"); - html += `
`; - html += `
${gpuData.name}
`; - html += `
`; - - if (gpuData.stats.engines) { - console.log("here2"); - // Render/3D - if (gpuData.stats.engines['Render/3D']) { - html += `Render/3D: ${gpuData.stats.engines['Render/3D'].busy}% | `; - } - - // Video - if (gpuData.stats.engines['Video']) { - html += `Video: ${gpuData.stats.engines['Video'].busy}% | `; - } - - // Blitter - if (gpuData.stats.engines['Blitter']) { - html += `Blitter: ${gpuData.stats.engines['Blitter'].busy}% | `; - } - - // VideoEnhance - if (gpuData.stats.engines['VideoEnhance']) { - html += `VideoEnhance: ${gpuData.stats.engines['VideoEnhance'].busy}% | `; - } - } - - // Power and Frequency info - html += `Power: ${gpuData.stats.power?.GPU ?? 'N/A'} / ${gpuData.stats.power?.Package ?? 'N/A'} ${gpuData.stats.power?.unit || 'W'}`; - html += ` | Freq: ${gpuData.stats.frequency?.actual ?? 'N/A'}/${gpuData.stats.frequency?.requested ?? 'N/A'} ${gpuData.frequency?.unit || 'MHz'}`; - - html += `
`; - }); - - // todo add NVIDIA - - // todo add NVIDIA - - return html; - }, - }, -EOF - ) - #endregion cpu widget heredoc - if [[ $? -ne 0 ]]; then - echo "Error: Failed to generate cpu widget code" >&2 - exit 1 - fi -} -#endregion widget generation functions # Function to uninstall the modification function uninstall_mod { @@ -1714,7 +654,7 @@ function uninstall_mod { check_root_privileges - if [[ -z $(grep -e "$res->{sensorsOutput}" "$NODES_PM_FILE") ]] && [[ -z $(grep -e "$res->{systemInfo}" "$NODES_PM_FILE") ]]; then + if [[ -z $(grep -e "\$res->{sensorsOutput}" "$NODES_PM_FILE") ]] && [[ -z $(grep -e "\$res->{systemInfo}" "$NODES_PM_FILE") ]]; then err "Mod is not installed." fi @@ -1725,7 +665,7 @@ function uninstall_mod { local latest_nodes_pm=$(find "$BACKUP_DIR" -name "Nodes.pm.*" -type f -printf '%T+ %p\n' 2>/dev/null | sort -r | head -n 1 | awk '{print $2}') if [ -n "$latest_nodes_pm" ]; then - # Remove the latest Nodes.pm file + # Restore the latest Nodes.pm file msgb "Restoring latest Nodes.pm from backup: $latest_nodes_pm to \"$NODES_PM_FILE\"." cp "$latest_nodes_pm" "$NODES_PM_FILE" info "Restored Nodes.pm successfully." @@ -1733,11 +673,11 @@ function uninstall_mod { warn "No Nodes.pm backup files found." fi - # Find the latest pvemanagerlib.js file using the find command + # Restore original pvemanagerlib.js (uncomment the StatusView definition and remove loader) local latest_pvemanagerlibjs=$(find "$BACKUP_DIR" -name "pvemanagerlib.js.*" -type f -printf '%T+ %p\n' 2>/dev/null | sort -r | head -n 1 | awk '{print $2}') if [ -n "$latest_pvemanagerlibjs" ]; then - # Remove the latest pvemanagerlib.js file + # Restore the latest pvemanagerlib.js file msgb "Restoring latest pvemanagerlib.js from backup: $latest_pvemanagerlibjs to \"$PVE_MANAGER_LIB_JS_FILE\"." cp "$latest_pvemanagerlibjs" "$PVE_MANAGER_LIB_JS_FILE" info "Restored pvemanagerlib.js successfully." @@ -1745,25 +685,34 @@ function uninstall_mod { warn "No pvemanagerlib.js backup files found." fi - # Find the latest GPUMonitor.pm file using the find command - local latest_gpumonitor_pm=$(find "$BACKUP_DIR" -name "GPUMonitor.pm.*" -type f -printf '%T+ %p\n' 2>/dev/null | sort -r | head -n 1 | awk '{print $2}') - - if [ -n "$latest_gpumonitor_pm" ]; then - # Restore the latest GPUMonitor.pm file - msgb "Restoring latest GPUMonitor.pm from backup: $latest_gpumonitor_pm to \"$PVE_SENSOR_INFO_MOD_FILE\"." - cp "$latest_gpumonitor_pm" "$PVE_SENSOR_INFO_MOD_FILE" - info "Restored GPUMonitor.pm successfully." - elif [ -f "$PVE_SENSOR_INFO_MOD_FILE" ]; then - # No backup found but file exists, remove it - msgb "No GPUMonitor.pm backup found. Removing installed module: $PVE_SENSOR_INFO_MOD_FILE" - rm "$PVE_SENSOR_INFO_MOD_FILE" - info "Removed GPUMonitor.pm successfully." + # Remove UI module files + if [ -f "$PVE_MOD_JS_TARGET_FILE" ]; then + msgb "Removing UI module: $PVE_MOD_JS_TARGET_FILE" + rm "$PVE_MOD_JS_TARGET_FILE" + info "Removed UI module successfully." else - warn "No GPUMonitor.pm backup files found and module not installed." + warn "UI module file not found: $PVE_MOD_JS_TARGET_FILE" fi - if [ -n "$latest_nodes_pm" ] || [ -n "$latest_pvemanagerlibjs" ] || [ -n "$latest_gpumonitor_pm" ] || [ -f "$PVE_SENSOR_INFO_MOD_FILE" ]; then - # At least one of the variables is not empty, restart the proxy + # Remove Sensor Info Perl module + local latest_sensor_info_pm=$(find "$BACKUP_DIR" -name "PveMod_SensorInfo.pm.*" -type f -printf '%T+ %p\n' 2>/dev/null | sort -r | head -n 1 | awk '{print $2}') + + if [ -n "$latest_sensor_info_pm" ]; then + # Restore the latest PveMod_SensorInfo.pm file (if there's a backup) + msgb "Restoring latest PveMod_SensorInfo.pm from backup: $latest_sensor_info_pm to \"$PVE_SENSOR_INFO_MOD_FILE\"." + cp "$latest_sensor_info_pm" "$PVE_SENSOR_INFO_MOD_FILE" + info "Restored PveMod_SensorInfo.pm successfully." + elif [ -f "$PVE_SENSOR_INFO_MOD_FILE" ]; then + # No backup found but file exists, remove it + msgb "No PveMod_SensorInfo.pm backup found. Removing installed module: $PVE_SENSOR_INFO_MOD_FILE" + rm "$PVE_SENSOR_INFO_MOD_FILE" + info "Removed PveMod_SensorInfo.pm successfully." + else + warn "No PveMod_SensorInfo.pm backup files found and module not installed." + fi + + if [ -n "$latest_nodes_pm" ] || [ -n "$latest_pvemanagerlibjs" ] || [ -f "$PVE_MOD_JS_TARGET_FILE" ] || [ -n "$latest_sensor_info_pm" ] || [ -f "$PVE_SENSOR_INFO_MOD_FILE" ]; then + # At least one file was modified, restart the proxy restart_proxy fi @@ -1772,11 +721,12 @@ function uninstall_mod { # Function to check if the modification is installed check_mod_installation() { - if [[ -n $(grep -F 'use PVE::API2::GPUMonitor' "$NODES_PM_FILE") ]] || \ + if [[ -n $(grep -F 'use PVE::API2::PveMod_SensorInfo' "$NODES_PM_FILE") ]] || \ + [[ -n $(grep -F 'use PVE::API2::GPUMonitor' "$NODES_PM_FILE") ]] || \ [[ -n $(grep -F '$res->{sensorsJSONOutput}' "$NODES_PM_FILE") ]] || \ [[ -n $(grep -F '$res->{sensorsOutput}' "$NODES_PM_FILE") ]] || \ - [[ -n $(grep -F '$res->{systemInfo}' "$NODES_PM_FILE") ]] && \ - [[ -n $(grep -E "itemId: 'thermal[[:alnum:]]*'" "$PVE_MANAGER_LIB_JS_FILE") ]]; then + [[ -n $(grep -F '$res->{systemInfo}' "$NODES_PM_FILE") ]] || \ + [[ -f "$PVE_MOD_JS_TARGET_FILE" ]]; then err "Mod is already installed. Uninstall existing before installing." fi } @@ -1887,7 +837,7 @@ function perform_backup { create_file_backup "$NODES_PM_FILE" "$timestamp" create_file_backup "$PVE_MANAGER_LIB_JS_FILE" "$timestamp" - # Backup GPU Monitor module if it exists + # Backup Sensor Info module if it exists if [[ -f "$PVE_SENSOR_INFO_MOD_FILE" ]]; then create_file_backup "$PVE_SENSOR_INFO_MOD_FILE" "$timestamp" fi