PR review changes

This commit is contained in:
Meliox 2026-08-22 23:07:58 +02:00
parent 37aea12483
commit 955cc7941c

View File

@ -899,242 +899,297 @@ Ext.define('PVE.node.StatusView', {
}, },
}, },
{ {
itemId: 'upsc', itemId: 'upsc',
colspan: 2, colspan: 2,
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', valueField: 'PveMod_upsInfo',
printBar: true, printBar: true,
warningThreshold: 1.1, warningThreshold: 1.1,
criticalThreshold: 1.2, criticalThreshold: 1.2,
layout: { layout: {
type: 'vbox', type: 'vbox',
align: 'stretch', align: 'stretch',
}, },
items: [ items: [
{ {
xtype: 'component', xtype: 'component',
itemId: 'label', itemId: 'label',
data: { data: {
title: '', title: '',
usage: '', usage: '',
iconCls: undefined, iconCls: undefined,
}, },
tpl: [ tpl: [
'<div class="left-aligned">', '<div class="left-aligned">',
'<tpl if="iconCls">', '<tpl if="iconCls">',
'<i class="{iconCls}"></i> ', '<i class="{iconCls}"></i> ',
'</tpl>', '</tpl>',
'{title}</div>', '{title}</div>',
], ],
}, },
{ {
xtype: 'container', xtype: 'container',
layout: { layout: {
type: 'hbox', type: 'hbox',
align: 'middle', align: 'middle',
}, },
items: [ items: [
{ {
xtype: 'component', xtype: 'component',
itemId: 'usageText', itemId: 'usageText',
flex: 1, flex: 1,
minWidth: 0, minWidth: 0,
margin: '0 16 0 0', margin: '0 16 0 0',
}, },
{ {
xtype: 'progressbar', xtype: 'container',
itemId: 'progress', flex: 1,
flex: 1, minWidth: 0,
minWidth: 0, layout: {
height: 5, type: 'vbox',
value: 0, align: 'stretch',
animate: true, },
}, items: [
], {
}, xtype: 'component',
], itemId: 'loadText',
initComponent: function() { margin: '0 0 2 0',
var me = this; },
if (!me.title) { {
throw 'no title defined'; xtype: 'progressbar',
} itemId: 'progress',
Ext.container.Container.prototype.initComponent.call(me); height: 5,
var progress = me.down('#progress'); value: 0,
if (progress) { animate: true,
progress.setVisible(!!me.printBar); },
} ],
me.updateValue(me.text, me.value); },
me.setIconCls(me.iconCls); ],
}, },
setPrintBar: function(enable) { ],
var me = this; initComponent: function() {
me.printBar = enable; var me = this;
var progress = me.down('#progress'); if (!me.title) {
if (progress) { throw 'no title defined';
progress.setVisible(enable); }
} Ext.container.Container.prototype.initComponent.call(me);
}, var progress = me.down('#progress');
updateValue: function(text, usage) { if (progress) {
var me = this; progress.setVisible(!!me.printBar);
if (me.lastText === text && me.lastUsage === usage) { }
return; me.updateValue(me.text, me.value);
} me.setIconCls(me.iconCls);
me.lastText = text; },
me.lastUsage = usage; 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 || '';
var label = me.getComponent('label'); if (me.lastText === text && me.lastUsage === usage && me.lastLoadText === loadText) {
if (label) { return;
label.update(Ext.apply(label.data, { title: me.title, usage: '' })); }
} me.lastText = text;
me.lastUsage = usage;
me.lastLoadText = loadText;
var usageText = me.down('#usageText'); var label = me.getComponent('label');
if (usageText) { if (label) {
if (usageText.setHtml) { label.update(Ext.apply(label.data, { title: me.title, usage: '' }));
usageText.setHtml(text || ''); }
} else {
usageText.update(text || '');
}
}
var progressBar = me.down('#progress'); var usageText = me.down('#usageText');
if (usage !== undefined && me.printBar && Ext.isNumeric(usage) && usage >= 0 && progressBar) { if (usageText) {
progressBar.updateProgress(usage, ''); if (usageText.setHtml) {
if (usage > me.criticalThreshold) { usageText.setHtml(text || '');
progressBar.removeCls('warning'); } else {
progressBar.addCls('critical'); usageText.update(text || '');
} else if (usage > me.warningThreshold) { }
progressBar.removeCls('critical'); }
progressBar.addCls('warning');
} else {
progressBar.removeCls('warning');
progressBar.removeCls('critical');
}
}
},
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));
},
renderer: function(value) {
let objValue;
try {
objValue = value || {};
} catch (e) {
objValue = {};
}
if (objValue.disabled === true) { var loadTextCmp = me.down('#loadText');
this.hide(); if (loadTextCmp) {
this.setPrintBar(false); if (loadTextCmp.setHtml) {
return ''; loadTextCmp.setHtml(loadText);
} } else {
loadTextCmp.update(loadText);
}
}
const upsKeys = Object.keys(objValue).filter(function(k) { var progressBar = me.down('#progress');
return objValue[k] && typeof objValue[k] === 'object' && !Array.isArray(objValue[k]); if (usage !== undefined && me.printBar && Ext.isNumeric(usage) && usage >= 0 && progressBar) {
}); progressBar.updateProgress(usage, '');
if (!upsKeys.length) { if (usage > me.criticalThreshold) {
this.hide(); progressBar.removeCls('warning');
this.setPrintBar(false); progressBar.addCls('critical');
return ''; } else if (usage > me.warningThreshold) {
} progressBar.removeCls('critical');
this.show(); progressBar.addCls('warning');
this.setPrintBar(true); } 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;
try {
objValue = value || {};
} catch (e) {
objValue = {};
}
function formatRuntime(seconds) { if (objValue.disabled === true) {
const s = parseInt(seconds, 10); this.hide();
if (!s || isNaN(s)) { this.setPrintBar(false);
return null; return '';
} }
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 upsKeys = Object.keys(objValue).filter(function(k) {
const u = String(upsStatus || '').toUpperCase(); return objValue[k] && typeof objValue[k] === 'object' && !Array.isArray(objValue[k]);
if (u.indexOf('LB') >= 0) { });
return { text: 'Low Battery', color: '#d9534f' }; if (!upsKeys.length) {
} this.hide();
if (u.indexOf('OB') >= 0) { this.setPrintBar(false);
return { text: 'On Battery', color: '#d9534f' }; return '';
} }
if (u.indexOf('FSD') >= 0) { this.show();
return { text: 'Shutdown', color: '#d9534f' }; this.setPrintBar(true);
}
if (u.indexOf('OL') >= 0) {
return {
text: u.indexOf('CHRG') >= 0 ? 'Online, charging' : 'Online',
color: null,
};
}
return { text: upsStatus || 'Unknown', color: '#f0ad4e' };
}
function colorize(label, color) { function formatRuntime(seconds) {
if (!color) { const s = parseInt(seconds, 10);
return label; if (!s || isNaN(s)) {
} return null;
return '<span style="color:' + color + ';">' + label + '</span>'; }
} const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
if (h > 0) {
return h + 'h ' + m + 'm';
}
return m + 'm';
}
const rows = []; function statusText(upsStatus) {
upsKeys.forEach(function(upsKey) { const u = String(upsStatus || '').toUpperCase();
const upsData = objValue[upsKey] || {}; if (u.indexOf('LB') >= 0) {
const charge = parseFloat(upsData['battery.charge']); return { text: 'Low Battery', color: '#d9534f' };
const runtime = formatRuntime(upsData['battery.runtime']); }
const inputVoltage = parseFloat(upsData['input.voltage']); if (u.indexOf('OB') >= 0) {
const load = parseFloat(upsData['ups.load']); return { text: 'On Battery', color: '#d9534f' };
const watts = parseFloat(upsData['ups.realpower']); }
const model = upsData['ups.model'] || upsData['device.model'] || upsKey; if (u.indexOf('FSD') >= 0) {
const st = statusText(upsData['ups.status']); return { text: 'Shutdown', color: '#d9534f' };
const bits = []; }
if (st.text) { if (u.indexOf('OL') >= 0) {
bits.push(colorize(st.text, st.color)); return {
} text: u.indexOf('CHRG') >= 0 ? 'Online, charging' : 'Online',
if (!isNaN(charge)) { color: null,
bits.push('Battery ' + Math.round(charge) + '%'); };
} }
if (!isNaN(watts)) { return { text: upsStatus || 'Unknown', color: '#f0ad4e' };
bits.push(Math.round(watts) + ' W'); }
}
if (!isNaN(load)) {
bits.push('Load ' + Math.round(load) + '%');
}
if (runtime) {
bits.push(runtime + ' left');
}
if (!isNaN(inputVoltage)) {
const places = inputVoltage >= 50 ? 0 : 1;
bits.push(inputVoltage.toFixed(places) + ' V in');
}
rows.push( function colorize(label, color) {
'<tr>' + if (!color) {
'<td style="padding: 2px 10px 2px 0; text-align: left; width: 30%; vertical-align: top; overflow-wrap: anywhere; word-break: break-word;">' + model + '</td>' + return label;
'<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;">' + bits.join(' | ') + '</td>' + }
'</tr>' return '<span style="color:' + color + ';">' + label + '</span>';
); }
});
return '<div style="padding-left: 20px; box-sizing: border-box;"><table style="width: 100%; border-collapse: collapse; table-layout: fixed;">' + rows.join('') + '</table></div>'; 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']);
const testResult = upsData['ups.test.result'];
const manufacturingDate = 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>';
// 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);
}
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>'
);
});
// 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>';
}
},
{ {
xtype: 'box', xtype: 'box',
colspan: 2, colspan: 2,