\ No newline at end of file
diff --git a/core/template/mobile/AsusWRT.html b/core/template/mobile/AsusWRT.html
index 8e5bdacc..4c3b425c 100644
--- a/core/template/mobile/AsusWRT.html
+++ b/core/template/mobile/AsusWRT.html
@@ -1,4 +1,6 @@
-
+
\ No newline at end of file
diff --git a/desktop/js/Monitoring.js b/desktop/js/Monitoring.js
index 4bc87368..0e64252e 100644
--- a/desktop/js/Monitoring.js
+++ b/desktop/js/Monitoring.js
@@ -14,179 +14,487 @@
* along with Jeedom. If not, see
.
*/
-/* Fonction permettant l'affichage des commandes dans l'équipement */
+// Protect against multiple script loads (Jeedom SPA navigation, cache, etc.)
+(() => {
+'use strict'
+
+// DOM Selectors constants (better minification + no string repetition + immutable)
+const SELECTORS = Object.freeze({
+ TABLE_CMD: '#table_cmd',
+ EQ_ID: '.eqLogicAttr[data-l1key=id]',
+ SYNO_CHECKBOX: '.eqLogicAttr[data-l2key=synology]',
+ QNAP_CHECKBOX: '.eqLogicAttr[data-l2key=qnap]',
+ ASUS_CHECKBOX: '.eqLogicAttr[data-l2key=asuswrt]',
+ SYNO_CONF: '.syno_conf',
+ ASUS_CONF: '.asuswrt_conf'
+})
+
+// Liste des commandes pouvant être historisées (en constant pour performance)
+const HISTORIZED_COMMANDS = Object.freeze([
+ 'cron_status', 'uptime_sec', 'load_avg_1mn', 'load_avg_5mn', 'load_avg_15mn',
+ 'memory_total', 'memory_used', 'memory_free', 'memory_buffcache', 'memory_available',
+ 'memory_free_percent', 'memory_used_percent', 'memory_available_percent',
+ 'swap_free_percent', 'swap_used_percent', 'swap_total', 'swap_used', 'swap_free',
+ 'network_tx', 'network_rx', 'hdd_total', 'hdd_used', 'hdd_free',
+ 'hdd_used_percent', 'hdd_free_percent', 'cpu_temp',
+ 'perso1', 'perso2', 'perso3', 'perso4',
+ 'syno_hddv2_total', 'syno_hddv2_used', 'syno_hddv2_free', 'syno_hddv2_used_percent', 'syno_hddv2_free_percent',
+ 'syno_hddusb_total', 'syno_hddusb_used', 'syno_hddusb_used_percent', 'syno_hddusb_free', 'syno_hddusb_free_percent',
+ 'syno_hddesata_total', 'syno_hddesata_used', 'syno_hddesata_used_percent', 'syno_hddesata_free', 'syno_hddesata_free_percent',
+ 'asus_clients_total', 'asus_clients_wifi24', 'asus_clients_wifi5', 'asus_clients_wired',
+ 'asus_fw_check', 'asus_wifi2g_temp', 'asus_wifi5g_temp'
+])
+
+// Commandes avec seuils verts (bas) vers rouges (haut)
+const GREEN_TO_RED_COMMANDS = Object.freeze([
+ 'load_avg_1mn', 'load_avg_5mn', 'load_avg_15mn',
+ 'memory_used_percent', 'swap_used_percent', 'cpu_temp',
+ 'hdd_used_percent', 'syno_hddv2_used_percent', 'syno_hddusb_used_percent', 'syno_hddesata_used_percent'
+])
+
+// Commandes avec seuils rouges (bas) vers verts (haut)
+const RED_TO_GREEN_COMMANDS = Object.freeze([
+ 'memory_free_percent', 'swap_free_percent', 'memory_available_percent',
+ 'hdd_free_percent', 'syno_hddv2_free_percent', 'syno_hddusb_free_percent', 'syno_hddesata_free_percent'
+])
+
+// Commandes personnalisables
+const CUSTOM_COMMANDS = Object.freeze(['perso1', 'perso2', 'perso3', 'perso4'])
+
+// Commandes liées (pour select value)
+const LINKED_COMMANDS = Object.freeze(['cron_status', 'cron_on', 'cron_off'])
+
+/**
+ * Helper pour générer les champs de seuils de couleurs
+ */
+const buildColorThresholds = (logicalId, reverseColors = false) => {
+ if (reverseColors) {
+ return `
[{{Rouge}}] < ≤ [{{Orange}}] ≤ < [{{Vert}}]`
+ }
+ return `
[{{Vert}}] < ≤ [{{Orange}}] ≤ < [{{Rouge}}]`
+}
+
+/**
+ * Fonction permettant l'affichage des commandes dans l'équipement
+ * @param {Object} _cmd - Commande à ajouter
+ */
function addCmdToTable(_cmd) {
- if (!isset(_cmd)) {
- var _cmd = { configuration: {} };
- }
- if (!isset(_cmd.configuration)) {
- _cmd.configuration = {}
- }
-
- let tr = '
';
- tr += '';
- tr += ' ';
- tr += ' ';
- tr += '';
- tr += '';
-
- if (['cron_status', 'cron_on', 'cron_off'].includes(init(_cmd.logicalId))) {
- tr += '';
- tr += '{{Aucune}} ';
- tr += ' ';
- }
- tr += ' ';
- tr += '';
- if (['load_avg_1mn', 'load_avg_5mn', 'load_avg_15mn', 'memory_used_percent', 'swap_used_percent', 'cpu_temp', 'hdd_used_percent', 'syno_hddv2_used_percent', 'syno_hddusb_used_percent', 'syno_hddesata_used_percent'].includes(init(_cmd.logicalId))) {
- tr += '[{{Vert}}] \< \u{2264} [{{Orange}}] \u{2264} \< [{{Rouge}}] ';
- }
- if (['memory_free_percent', 'swap_free_percent', 'memory_available_percent', 'hdd_free_percent', 'syno_hddv2_free_percent', 'syno_hddusb_free_percent', 'syno_hddesata_free_percent'].includes(init(_cmd.logicalId))) {
- tr += '[{{Rouge}}] \< \u{2264} [{{Orange}}] \u{2264} \< [{{Vert}}] ';
- }
- if (['perso1', 'perso2', 'perso3', 'perso4'].includes(init(_cmd.logicalId))) {
- tr += ' ';
- tr += ' {{Unité}} : ';
- tr += '[{{Vert}}] \< \u{2264} [{{Orange}}] \u{2264} \< [{{Rouge}}] ';
- }
- tr += ' ';
- tr += '';
- tr += ' {{Afficher}} ';
-
- // Liste des commandes de base pouvant être historisées
- let historizedCommands = ['cron_status', 'uptime_sec', 'load_avg_1mn', 'load_avg_5mn', 'load_avg_15mn', 'memory_total', 'memory_used', 'memory_free', 'memory_buffcache', 'memory_available', 'memory_free_percent', 'memory_used_percent', 'memory_available_percent', 'swap_free_percent', 'swap_used_percent', 'swap_total', 'swap_used', 'swap_free', 'network_tx', 'network_rx', 'hdd_total', 'hdd_used', 'hdd_free', 'hdd_used_percent', 'hdd_free_percent', 'cpu_temp', 'perso1', 'perso2', 'perso3', 'perso4', 'syno_hddv2_total', 'syno_hddv2_used', 'syno_hddv2_free', 'syno_hddv2_used_percent', 'syno_hddv2_free_percent', 'syno_hddusb_total', 'syno_hddusb_used', 'syno_hddusb_used_percent', 'syno_hddusb_free', 'syno_hddusb_free_percent', 'syno_hddesata_total', 'syno_hddesata_used', 'syno_hddesata_used_percent', 'syno_hddesata_free', 'syno_hddesata_free_percent', 'asus_clients_total', 'asus_clients_wifi24', 'asus_clients_wifi5', 'asus_clients_wired', 'asus_fw_check', 'asus_wifi2g_temp', 'asus_wifi5g_temp'];
-
- // Vérifier si c'est une commande standard ou une commande de carte réseau supplémentaire
- let canBeHistorized = historizedCommands.includes(init(_cmd.logicalId)) ||
- init(_cmd.logicalId).startsWith('network_tx_') ||
- init(_cmd.logicalId).startsWith('network_rx_');
-
- if (canBeHistorized) {
- tr += ' {{Historiser}} ';
- }
- tr += ' ';
- tr += '';
- if (['perso1', 'perso2', 'perso3', 'perso4', 'cron_status'].includes(init(_cmd.logicalId))) {
- tr += ' ';
- tr += ' ';
- }
- tr += ' ';
- tr += '';
- tr += ' ';
- tr += ' ';
-
- tr += '';
- if (is_numeric(_cmd.id)) {
- tr += ' ';
- tr += ' {{Tester}} ';
- }
- tr += ' ';
- tr += ' ';
- tr += ' ';
-
- let newRow = document.createElement('tr')
- newRow.innerHTML = tr
- newRow.addClass('cmd')
- newRow.setAttribute('data-cmd_id', init(_cmd.id))
- document.getElementById('table_cmd').querySelector('tbody').appendChild(newRow)
-
- jeedom.eqLogic.buildSelectCmd({
- id: document.querySelector('.eqLogicAttr[data-l1key="id"]').jeeValue(),
- filter: { type: 'info' },
- error: function(error) {
- jeedomUtils.showAlert({ message: error.message, level: 'danger' })
- },
- success: function(result) {
- newRow.querySelector('.cmdAttr[data-l1key="value"]')?.insertAdjacentHTML('beforeend', result)
- newRow.setJeeValues(_cmd, '.cmdAttr')
- jeedom.cmd.changeType(newRow, init(_cmd.subType))
- }
- })
+ if (!isset(_cmd)) {
+ var _cmd = { configuration: {} }
+ }
+ if (!isset(_cmd.configuration)) {
+ _cmd.configuration = {}
+ }
+
+ const logicalId = init(_cmd.logicalId)
+
+ // Vérifier si c'est une commande standard ou une commande de carte réseau supplémentaire
+ const canBeHistorized = HISTORIZED_COMMANDS.includes(logicalId) ||
+ logicalId.startsWith('network_tx_') ||
+ logicalId.startsWith('network_rx_')
+
+ // Déterminer le type de configuration
+ const hasGreenToRed = GREEN_TO_RED_COMMANDS.includes(logicalId)
+ const hasRedToGreen = RED_TO_GREEN_COMMANDS.includes(logicalId)
+ const isCustom = CUSTOM_COMMANDS.includes(logicalId)
+ const isLinked = LINKED_COMMANDS.includes(logicalId)
+ const hasTypeSubType = isCustom || logicalId === 'cron_status'
+
+ // Générer les différentes parties du HTML
+ let configCell = ''
+ if (hasGreenToRed) {
+ configCell = buildColorThresholds(logicalId, false)
+ } else if (hasRedToGreen) {
+ configCell = buildColorThresholds(logicalId, true)
+ } else if (isCustom) {
+ configCell = `
+
{{Unité}} :
+
${buildColorThresholds(logicalId, false)}`
+ }
+
+ // Construction du HTML avec template literals (optimal V8 performance)
+ const testButtons = is_numeric(_cmd.id)
+ ? '
{{Tester}} '
+ : ''
+
+ const rowHtml = `
+
+
+
+ ${isLinked ? '{{Aucune}} ' : ''}
+
+
${configCell}
+
+ {{Afficher}}
+ ${canBeHistorized ? ' {{Historiser}} ' : ''}
+
+
+ ${hasTypeSubType ? ` ` : ''}
+
+
+
+ ${testButtons}
+
+ `
+
+ // Create and configure row element (optimal: Object.assign for batch properties)
+ const newRow = Object.assign(document.createElement('tr'), {
+ className: 'cmd',
+ innerHTML: rowHtml
+ })
+ newRow.setAttribute('data-cmd_id', init(_cmd.id))
+
+ // Cache table body for performance
+ const tableBody = document.querySelector(`${SELECTORS.TABLE_CMD} tbody`)
+ if (!tableBody) return console.error('Table body not found')
+
+ tableBody.appendChild(newRow)
+
+ // Cache eqLogic ID to avoid multiple DOM queries
+ const eqLogicIdElement = document.querySelector(SELECTORS.EQ_ID)
+ if (!eqLogicIdElement) return console.error('Equipment ID element not found')
+
+ jeedom.eqLogic.buildSelectCmd({
+ id: eqLogicIdElement.jeeValue(),
+ filter: { type: 'info' },
+ error: function(error) {
+ jeedomUtils.showAlert({ message: error.message, level: 'danger' })
+ },
+ success: function(result) {
+ newRow.querySelector('.cmdAttr[data-l1key="value"]')?.insertAdjacentHTML('beforeend', result)
+ newRow.setJeeValues(_cmd, '.cmdAttr')
+ jeedom.cmd.changeType(newRow, init(_cmd.subType))
+ }
+ })
+}
+
+// Helper functions pour une meilleure réutilisation
+const updateCheckboxGroup = (checkbox, showElements = [], hideElements = [], uncheckElements = []) => {
+ if (!checkbox) return
+
+ if (checkbox.checked) {
+ showElements.forEach(el => el?.seen())
+ hideElements.forEach(el => el?.unseen()) // Masquer les blocs des autres options
+ uncheckElements.forEach(el => el?.jeeValue(0)) // Décocher les autres options
+ } else {
+ showElements.forEach(el => el?.unseen()) // Masquer le bloc de cette option
+ }
}
-document.querySelector(".eqLogicAttr[data-l2key='synology']").addEventListener('change', function() {
- if (this.checked) {
- document.querySelector(".syno_conf").style.display = "block";
- document.querySelector(".asuswrt_conf").style.display = "none";
- document.querySelector('input.eqLogicAttr[data-l2key="asuswrt"]').checked = false;
- document.querySelector('input.eqLogicAttr[data-l2key="qnap"]').checked = false;
- } else {
- document.querySelector(".syno_conf").style.display = "none";
- }
-});
-
-document.querySelector(".eqLogicAttr[data-l2key='qnap']").addEventListener('change', function() {
- if (this.checked) {
- document.querySelector('input.eqLogicAttr[data-l2key="asuswrt"]').checked = false;
- document.querySelector('input.eqLogicAttr[data-l2key="synology"]').checked = false;
- document.querySelector(".syno_conf").style.display = "none";
- document.querySelector(".asuswrt_conf").style.display = "none";
- }
-});
-
-document.querySelector(".eqLogicAttr[data-l2key='asuswrt']").addEventListener('change', function() {
- if (this.checked) {
- document.querySelector('input.eqLogicAttr[data-l2key="qnap"]').checked = false;
- document.querySelector('input.eqLogicAttr[data-l2key="synology"]').checked = false;
- document.querySelector(".syno_conf").style.display = "none";
- document.querySelector(".asuswrt_conf").style.display = "block";
- } else {
- document.querySelector(".asuswrt_conf").style.display = "none";
- }
-});
-
-document.querySelectorAll('.pluginAction[data-action=openLocation]').forEach(function (element) {
- element.addEventListener('click', function () {
- window.open(this.getAttribute("data-location"), "_blank", null);
- });
-});
-
-document.querySelector(".eqLogicAttr[data-l2key='syno_use_temp_path']").addEventListener('change', function () {
- if(this.checked){
- document.querySelector(".syno_conf_temppath").style.display = "block";
- } else {
- document.querySelector(".syno_conf_temppath").style.display = "none";
- }
-});
-
-document.querySelector(".eqLogicAttr[data-l2key='linux_use_temp_cmd']").addEventListener('change', function() {
- if (this.checked) {
- document.querySelector(".linux_class_temp_cmd").style.display = "block";
- } else {
- document.querySelector(".linux_class_temp_cmd").style.display = "none";
- }
-});
-
-document.querySelector(".eqLogicAttr[data-l2key='pull_use_custom']").addEventListener('change', function () {
- if(this.checked){
- document.querySelector(".pull_class").style.display = "block";
- } else {
- document.querySelector(".pull_class").style.display = "none";
- }
-});
-
-document.querySelector(".eqLogicAttr[data-l2key='multi_if']").addEventListener('change', function () {
- if(this.checked){
- document.querySelector(".multi_if_conf").style.display = "block";
- } else {
- document.querySelector(".multi_if_conf").style.display = "none";
- }
-});
-
-document.querySelector(".eqLogicAttr[data-l2key='localoudistant']").addEventListener('change', function () {
- if (this.selectedIndex == 1) {
- document.querySelector(".distant").style.display = "block";
- document.querySelector(".local").style.display = "none";
- } else {
- document.querySelector(".distant").style.display = "none";
- document.querySelector(".local").style.display = "block";
- }
-});
+// Handlers nommés pour pouvoir les remove/add proprement (spécifiques à chaque équipement)
+const handleSynologyChange = function(event) {
+ updateCheckboxGroup(
+ event.currentTarget,
+ [document.querySelector(SELECTORS.SYNO_CONF)],
+ [document.querySelector(SELECTORS.ASUS_CONF)],
+ [document.querySelector(SELECTORS.ASUS_CHECKBOX), document.querySelector(SELECTORS.QNAP_CHECKBOX)]
+ )
+}
+
+const handleQnapChange = function(event) {
+ if (event.currentTarget.checked) {
+ const asusCheckbox = document.querySelector(SELECTORS.ASUS_CHECKBOX)
+ const synoCheckbox = document.querySelector(SELECTORS.SYNO_CHECKBOX)
+ const synoConf = document.querySelector(SELECTORS.SYNO_CONF)
+ const asusConf = document.querySelector(SELECTORS.ASUS_CONF)
+
+ asusCheckbox?.jeeValue(0)
+ synoCheckbox?.jeeValue(0)
+ synoConf?.unseen()
+ asusConf?.unseen()
+ }
+}
+
+const handleAsusChange = function(event) {
+ updateCheckboxGroup(
+ event.currentTarget,
+ [document.querySelector(SELECTORS.ASUS_CONF)],
+ [document.querySelector(SELECTORS.SYNO_CONF)],
+ [document.querySelector(SELECTORS.QNAP_CHECKBOX), document.querySelector(SELECTORS.SYNO_CHECKBOX)]
+ )
+}
+
+const handleSynoTempPath = function(event) {
+ const tempPath = document.querySelector('.syno_conf_temppath')
+ if (event.currentTarget.checked) {
+ tempPath?.seen()
+ } else {
+ tempPath?.unseen()
+ }
+}
+
+const handleLinuxTempCmd = function(event) {
+ const tempCmd = document.querySelector('.linux_class_temp_cmd')
+ if (event.currentTarget.checked) {
+ tempCmd?.seen()
+ } else {
+ tempCmd?.unseen()
+ }
+}
+
+const handlePullCustom = function(event) {
+ const pullClass = document.querySelector('.pull_class')
+ if (event.currentTarget.checked) {
+ pullClass?.seen()
+ } else {
+ pullClass?.unseen()
+ }
+}
+
+const handleMultiIf = function(event) {
+ const multiIfConf = document.querySelector('.multi_if_conf')
+ if (event.currentTarget.checked) {
+ multiIfConf?.seen()
+ } else {
+ multiIfConf?.unseen()
+ }
+}
+
+const handleLocalDistant = function(event) {
+ const distantDiv = document.querySelector('.distant')
+ const localDiv = document.querySelector('.local')
+ const selectedValue = event.currentTarget.value
+
+ if (selectedValue === 'distant') {
+ distantDiv?.seen()
+ localDiv?.unseen()
+ } else {
+ distantDiv?.unseen()
+ localDiv?.seen()
+ }
+}
+
+// Event delegation pour openLocation (global, attaché une seule fois)
+if (!window.monitoringOpenLocationAttached) {
+ window.monitoringOpenLocationAttached = true
+
+ document.addEventListener('click', (event) => {
+ const target = event.target.closest('.pluginAction[data-action=openLocation]')
+ if (target) {
+ event.preventDefault()
+ window.open(target.getAttribute('data-location'), '_blank', null)
+ }
+ })
+}
function printEqLogic(_eqLogic) {
- buildSelectHost(_eqLogic.configuration.SSHHostId);
+ if (!_eqLogic) return
+
+ // Cache DOM elements once
+ const elements = {
+ synoCheckbox: document.querySelector(SELECTORS.SYNO_CHECKBOX),
+ qnapCheckbox: document.querySelector(SELECTORS.QNAP_CHECKBOX),
+ asusCheckbox: document.querySelector(SELECTORS.ASUS_CHECKBOX),
+ synoConf: document.querySelector(SELECTORS.SYNO_CONF),
+ asusConf: document.querySelector(SELECTORS.ASUS_CONF),
+ synoTempPath: document.querySelector('.eqLogicAttr[data-l2key="syno_use_temp_path"]'),
+ synoTempPathDiv: document.querySelector('.syno_conf_temppath'),
+ linuxTempCmd: document.querySelector('.eqLogicAttr[data-l2key="linux_use_temp_cmd"]'),
+ linuxTempCmdDiv: document.querySelector('.linux_class_temp_cmd'),
+ pullCustom: document.querySelector('.eqLogicAttr[data-l2key="pull_use_custom"]'),
+ pullDiv: document.querySelector('.pull_class'),
+ multiIf: document.querySelector('.eqLogicAttr[data-l2key="multi_if"]'),
+ multiIfDiv: document.querySelector('.multi_if_conf'),
+ localDistant: document.querySelector('.eqLogicAttr[data-l2key="localoudistant"]'),
+ distantDiv: document.querySelector('.distant'),
+ localDiv: document.querySelector('.local')
+ }
+
+ // Attach event listeners for equipment-specific checkboxes (re-attached on each equipment load)
+ if (elements.synoCheckbox) {
+ elements.synoCheckbox.removeEventListener('change', handleSynologyChange)
+ elements.synoCheckbox.addEventListener('change', handleSynologyChange)
+ // Initialiser l'affichage au chargement
+ if (elements.synoCheckbox.checked) {
+ elements.synoConf?.seen()
+ elements.asusConf?.unseen()
+ elements.asusCheckbox?.jeeValue(0)
+ elements.qnapCheckbox?.jeeValue(0)
+ } else {
+ elements.synoConf?.unseen()
+ }
+ }
+
+ if (elements.qnapCheckbox) {
+ elements.qnapCheckbox.removeEventListener('change', handleQnapChange)
+ elements.qnapCheckbox.addEventListener('change', handleQnapChange)
+ }
+
+ if (elements.asusCheckbox) {
+ elements.asusCheckbox.removeEventListener('change', handleAsusChange)
+ elements.asusCheckbox.addEventListener('change', handleAsusChange)
+ // Initialiser l'affichage au chargement
+ if (elements.asusCheckbox.checked) {
+ elements.asusConf?.seen()
+ elements.synoConf?.unseen()
+ elements.qnapCheckbox?.jeeValue(0)
+ elements.synoCheckbox?.jeeValue(0)
+ } else {
+ elements.asusConf?.unseen()
+ }
+ }
+
+ if (elements.synoTempPath) {
+ elements.synoTempPath.removeEventListener('change', handleSynoTempPath)
+ elements.synoTempPath.addEventListener('change', handleSynoTempPath)
+ // Initialiser l'affichage au chargement
+ if (elements.synoTempPath.checked) {
+ elements.synoTempPathDiv?.seen()
+ } else {
+ elements.synoTempPathDiv?.unseen()
+ }
+ }
+
+ if (elements.linuxTempCmd) {
+ elements.linuxTempCmd.removeEventListener('change', handleLinuxTempCmd)
+ elements.linuxTempCmd.addEventListener('change', handleLinuxTempCmd)
+ // Initialiser l'affichage au chargement
+ if (elements.linuxTempCmd.checked) {
+ elements.linuxTempCmdDiv?.seen()
+ } else {
+ elements.linuxTempCmdDiv?.unseen()
+ }
+ }
+
+ if (elements.pullCustom) {
+ elements.pullCustom.removeEventListener('change', handlePullCustom)
+ elements.pullCustom.addEventListener('change', handlePullCustom)
+ // Initialiser l'affichage au chargement
+ if (elements.pullCustom.checked) {
+ elements.pullDiv?.seen()
+ } else {
+ elements.pullDiv?.unseen()
+ }
+ }
+
+ if (elements.multiIf) {
+ elements.multiIf.removeEventListener('change', handleMultiIf)
+ elements.multiIf.addEventListener('change', handleMultiIf)
+ // Initialiser l'affichage au chargement
+ if (elements.multiIf.checked) {
+ elements.multiIfDiv?.seen()
+ } else {
+ elements.multiIfDiv?.unseen()
+ }
+ }
+
+ if (elements.localDistant) {
+ elements.localDistant.removeEventListener('change', handleLocalDistant)
+ elements.localDistant.addEventListener('change', handleLocalDistant)
+ // Initialiser l'affichage au chargement
+ if (elements.localDistant.value === 'distant') {
+ elements.distantDiv?.seen()
+ elements.localDiv?.unseen()
+ } else {
+ elements.distantDiv?.unseen()
+ elements.localDiv?.seen()
+ }
+ }
+
+ // Build SSH host select
+ const buildPromise = buildSelectHost(_eqLogic.configuration.SSHHostId)
+
+ // Toggle add/edit button based on SSH host selection
+ const sshHostSelect = document.querySelector('.sshmanagerHelper[data-helper="list"]')
+ if (sshHostSelect) {
+ // Remove existing listener to avoid duplicates
+ sshHostSelect.removeEventListener('change', toggleSSHButtons)
+ // Attach listener
+ sshHostSelect.addEventListener('change', toggleSSHButtons)
+
+ // Initialize button display - pass the value directly instead of waiting
+ if (buildPromise && buildPromise.then) {
+ buildPromise.then(() => {
+ toggleSSHButtons(_eqLogic.configuration.SSHHostId)
+ })
+ } else {
+ // Fallback if buildSelectHost didn't return a promise
+ toggleSSHButtons(_eqLogic.configuration.SSHHostId)
+ }
+ }
+}
+
+/**
+ * Toggle between add and edit SSH buttons based on selection
+ * @param {Event|string|number} eventOrValue - Either a change event or a direct value (SSHHostId)
+ */
+function toggleSSHButtons(eventOrValue) {
+ let selectedValue
+
+ // Check if it's a direct value (string/number) or an event object
+ if (typeof eventOrValue === 'string' || typeof eventOrValue === 'number') {
+ selectedValue = eventOrValue
+ } else if (eventOrValue?.target || eventOrValue?.currentTarget) {
+ // It's an event, extract value from it
+ selectedValue = eventOrValue.target?.value ?? eventOrValue.currentTarget?.value ?? eventOrValue.value
+ }
+
+ // If still no value, read directly from the select element as fallback
+ if (!selectedValue) {
+ const sshHostSelect = document.querySelector('.sshmanagerHelper[data-helper="list"]')
+ selectedValue = sshHostSelect?.value
+ }
+
+ const addBtn = document.querySelector('.sshmanagerHelper[data-helper="add"]')
+ const editBtn = document.querySelector('.sshmanagerHelper[data-helper="edit"]')
+
+ if (selectedValue && selectedValue !== '') {
+ // Host selected → show edit, hide add
+ if (addBtn) addBtn.style.display = 'none'
+ if (editBtn) editBtn.style.display = 'block'
+ } else {
+ // No host selected → show add, hide edit
+ if (addBtn) addBtn.style.display = 'block'
+ if (editBtn) editBtn.style.display = 'none'
+ }
}
+
+// Health button click handler
+const healthButton = document.querySelector('#bt_healthMonitoring')
+if (healthButton) {
+ healthButton.addEventListener('click', function() {
+ jeeDialog.dialog({
+ id: 'md_healthMonitoring',
+ title: '{{Santé des équipements Monitoring}}',
+ width: '95%',
+ height: '90%',
+ top: '5vh',
+ contentUrl: 'index.php?v=d&plugin=Monitoring&modal=health.monitoring',
+ defaultButtons: {},
+ buttons: {
+ close: {
+ label: '
{{Fermer}}',
+ className: 'success',
+ callback: {
+ click: function(event) {
+ event.target.closest('div.jeeDialog')._jeeDialog.close()
+ }
+ }
+ }
+ },
+ callback: function() {
+ if (typeof initModalHealthMonitoring === 'function') {
+ initModalHealthMonitoring()
+ }
+ },
+ onClose: function() {
+ // Clean up resources when modal is closed
+ if (typeof cleanupHealthMonitoring === 'function') {
+ cleanupHealthMonitoring()
+ }
+ }
+ })
+ })
+}
+
+// Expose functions globally for Jeedom to call them
+window.addCmdToTable = addCmdToTable
+window.printEqLogic = printEqLogic
+
+})() // End of IIFE protection
+
diff --git a/desktop/js/health.monitoring.js b/desktop/js/health.monitoring.js
new file mode 100644
index 00000000..125b6d83
--- /dev/null
+++ b/desktop/js/health.monitoring.js
@@ -0,0 +1,476 @@
+/* This file is part of Jeedom.
+ *
+ * Jeedom is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Jeedom is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Jeedom. If not, see
.
+ */
+
+(() => {
+'use strict'
+
+// ========================================
+// === SELECTORS ===
+// ========================================
+
+const SELECTORS = {
+ TABLE_BODY: '#table_healthMonitoring tbody'
+}
+
+// Global handler reference to ensure single event listener
+let healthCmdUpdateHandler = null
+
+// ========================================
+// === CORE FUNCTIONS ===
+// ========================================
+
+/**
+ * Initialize health monitoring modal
+ * Called from modal PHP file
+ */
+const initModalHealthMonitoring = () => {
+ loadHealthData()
+}
+
+/**
+ * Clean up resources when modal is closed
+ * Removes event listeners to prevent memory leaks
+ */
+const cleanupHealthMonitoring = () => {
+ // Remove WebSocket event listener
+ if (healthCmdUpdateHandler) {
+ document.body.removeEventListener('cmd::update', healthCmdUpdateHandler)
+ healthCmdUpdateHandler = null
+ }
+
+ // Clear search input event listeners (handled by DOM removal)
+ // Clear button event listeners (handled by DOM removal)
+ // Table elements are automatically cleaned when modal DOM is removed
+}
+
+/**
+ * Load health data from backend
+ */
+const loadHealthData = () => {
+ domUtils.ajax({
+ type: 'POST',
+ url: 'plugins/Monitoring/core/ajax/Monitoring.ajax.php',
+ data: { action: 'getHealthData' },
+ dataType: 'json',
+ error: (error) => {
+ const tbody = document.querySelector(SELECTORS.TABLE_BODY)
+ if (tbody) {
+ tbody.innerHTML = `
${error.message}`
+ }
+ },
+ success: (data) => {
+ displayHealthData(data.result)
+ }
+ })
+}
+
+/**
+ * Display health data in table
+ * @param {Array} healthData - Array of equipment health data
+ */
+const displayHealthData = (healthData) => {
+ const tbody = document.querySelector(SELECTORS.TABLE_BODY)
+ if (!tbody) return
+
+ if (!healthData || healthData.length === 0) {
+ tbody.innerHTML = '
{{Aucun équipement trouvé}} '
+ return
+ }
+
+ const html = healthData.map(eqLogic => {
+ const isActive = eqLogic.isEnable === 1
+ const isVisible = eqLogic.isVisible === 1
+
+ let typeLabel = ''
+ switch (eqLogic.type) {
+ case 'local':
+ typeLabel = '
Local '
+ break
+ case 'distant':
+ typeLabel = '
Distant '
+ break
+ case 'unconfigured':
+ typeLabel = '
{{Non configuré}} '
+ break
+ default:
+ typeLabel = '
- '
+ }
+
+ // Prepare searchable values
+ const sshStatusSearch = eqLogic.commands?.sshStatus?.value === 'OK' ? 'OK' : eqLogic.commands?.sshStatus?.value === 'KO' ? 'KO' : ''
+ const cronValue = eqLogic.commands?.cronStatus?.value
+ const cronStatusSearch = cronValue ? ((cronValue === '1' || cronValue === 1 || cronValue === 'Yes') ? 'ON' : 'OFF') : ''
+
+ // Prepare cron custom data
+ const cronCustomValue = eqLogic.cronCustom || 0
+ const cronCustomData = { value: cronCustomValue }
+
+ return `
+
+ ${eqLogic.name}
+ ${isActive ? ' ' : ' '}
+ ${isVisible ? ' ' : ' '}
+ ${typeLabel}
+ ${eqLogic.sshHostName || '- '}
+ ${formatCmdValue(eqLogic.commands?.sshStatus, 'ssh')}
+ ${formatCmdValue(eqLogic.commands?.cronStatus, 'cron', eqLogic.type, cronCustomData)}
+ ${formatCmdValue(eqLogic.commands?.uptime)}
+ ${formatCmdValue(eqLogic.commands?.loadAvg1)}
+ ${formatCmdValue(eqLogic.commands?.loadAvg5)}
+ ${formatCmdValue(eqLogic.commands?.loadAvg15)}
+ ${formatCmdValue(eqLogic.commands?.ip)}
+ ${formatDate(eqLogic.lastRefresh, eqLogic.type)}
+
+ `
+ }).join('')
+
+ tbody.innerHTML = html
+
+ // Enrich data-search with formatted text for cells with formatted content
+ // Exclude status cells to preserve exact match
+ tbody.querySelectorAll('td[data-search]:not([data-type="status"])').forEach(cell => {
+ const currentSearch = cell.getAttribute('data-search')
+ const textContent = cell.textContent.trim()
+
+ // If textContent is different from data-search, add it
+ if (textContent && textContent !== currentSearch && textContent !== '-') {
+ cell.setAttribute('data-search', `${currentSearch} ${textContent}`)
+ }
+ })
+
+ // Initialize Jeedom tooltips with HTML support
+ initTooltips()
+
+ // Initialize DataTables for sorting only (no search)
+ jeedomUtils.initDataTables('#healthMonitoringContainer', false, false, [{ select: 0, sort: "asc" }])
+
+ // Custom search implementation
+ const searchInput = document.getElementById('healthSearchInput')
+ const tableRows = tbody.querySelectorAll('tr')
+
+ // Reusable search function
+ const performSearch = () => {
+ const searchTerm = searchInput ? searchInput.value.toLowerCase().trim() : ''
+
+ tableRows.forEach(row => {
+ if (searchTerm === '') {
+ row.style.display = ''
+ return
+ }
+
+ // List of status keywords that need exact match
+ const statusKeywords = ['on', 'off', 'ok', 'ko', 'actif', 'inactif', 'visible', 'invisible']
+ const isStatusKeyword = statusKeywords.includes(searchTerm)
+
+ const cells = row.querySelectorAll('td')
+ let found = false
+
+ for (let cell of cells) {
+ const isStatusCell = cell.getAttribute('data-type') === 'status'
+ const searchValue = cell.getAttribute('data-search')
+
+ if (!searchValue) continue
+
+ const searchLower = searchValue.toLowerCase()
+
+ // For status keywords: only search in status cells with exact match
+ if (isStatusKeyword) {
+ if (isStatusCell && searchLower === searchTerm) {
+ found = true
+ break
+ }
+ }
+ // For non-status keywords: contains search ONLY in non-status cells
+ else if (!isStatusCell) {
+ if (searchLower.includes(searchTerm)) {
+ found = true
+ break
+ }
+ }
+ }
+
+ row.style.display = found ? '' : 'none'
+ })
+ }
+
+ if (searchInput) {
+ searchInput.addEventListener('keyup', performSearch)
+ }
+
+ // Clear search button
+ const clearButton = document.getElementById('healthSearchClear')
+ if (clearButton && searchInput) {
+ clearButton.addEventListener('click', function() {
+ searchInput.value = ''
+ performSearch()
+ })
+ }
+
+ // Initialize Jeedom's automatic command update system for dynamically inserted elements
+ const cmdElements = tbody.querySelectorAll('.cmd[data-cmd_id]')
+
+ // Create mappings for efficient updates
+ const cmdMap = new Map() // cmd_id -> element
+ const eqLastCommMap = new Map() // eq_id -> last comm cell
+
+ cmdElements.forEach(element => {
+ const cmdId = element.getAttribute('data-cmd_id')
+ if (cmdId && cmdId !== '') {
+ cmdMap.set(cmdId, element)
+ }
+ })
+
+ tbody.querySelectorAll('.lastComm[data-eq-id]').forEach(cell => {
+ const eqId = cell.getAttribute('data-eq-id')
+ if (eqId) {
+ eqLastCommMap.set(eqId, cell)
+ }
+ })
+
+ if (cmdElements.length > 0) {
+ jeedom.cmd.refreshValue(cmdElements)
+ }
+
+ // Remove previous event listener if exists to avoid duplicates
+ if (healthCmdUpdateHandler) {
+ document.body.removeEventListener('cmd::update', healthCmdUpdateHandler)
+ }
+
+ // Single global event listener for cmd::update (performance optimization)
+ healthCmdUpdateHandler = (e) => {
+ if (!e.detail) return
+
+ const updates = Array.isArray(e.detail) ? e.detail : [e.detail]
+
+ updates.forEach(event => {
+ const cmdId = String(event.cmd_id || event.id)
+ const element = cmdMap.get(cmdId)
+
+ if (!element) return
+
+ const cmdType = element.getAttribute('data-cmd-type')
+ const value = event.display_value || event.value
+
+ // Update data-value attribute
+ element.setAttribute('data-value', value)
+
+ // Get parent cell to update data-search
+ const parentCell = element.closest('td')
+
+ // Format value based on command type
+ if (cmdType === 'ssh') {
+ element.innerHTML = formatCmdValue({ value: value }, 'ssh')
+ // Update data-search for status cell (exact match values)
+ if (parentCell) {
+ const searchValue = value === 'OK' ? 'OK' : value === 'KO' ? 'KO' : ''
+ parentCell.setAttribute('data-search', searchValue)
+ }
+ } else if (cmdType === 'cron') {
+ const eqType = element.getAttribute('data-eq-type')
+ const cronCustomValue = element.getAttribute('data-cron-custom')
+ const cronCustomData = cronCustomValue ? { value: cronCustomValue } : null
+ element.innerHTML = formatCmdValue({ value: value }, 'cron', eqType, cronCustomData)
+ // Update data-search for status cell (exact match values)
+ if (parentCell) {
+ const searchValue = (value === '1' || value === 1 || value === 'Yes') ? 'ON' : 'OFF'
+ parentCell.setAttribute('data-search', searchValue)
+ }
+ } else {
+ element.innerHTML = formatCmdValue({ value: value })
+ // Update data-search for non-status cells (include formatted text)
+ if (parentCell && !parentCell.getAttribute('data-type')) {
+ const formattedText = element.textContent.trim()
+ parentCell.setAttribute('data-search', `${value} ${formattedText}`)
+ }
+ }
+
+ // Refresh search after data-search update
+ performSearch()
+
+ // Add visual feedback for update
+ element.classList.remove('cmd-updated')
+ void element.offsetWidth
+ element.classList.add('cmd-updated')
+
+ // Remove class after animation completes
+ setTimeout(() => element.classList.remove('cmd-updated'), 2000)
+
+ // Update last communication date using direct mapping
+ if (event.collectDate) {
+ const eqId = element.getAttribute('data-eq-id')
+ if (eqId) {
+ const lastCommCell = eqLastCommMap.get(eqId)
+ if (lastCommCell) {
+ const eqType = lastCommCell.getAttribute('data-eq-type')
+ const formattedDate = formatDate(event.collectDate, eqType)
+ const lastCommSpan = lastCommCell.querySelector('span')
+
+ if (lastCommSpan) {
+ lastCommSpan.innerHTML = formattedDate
+
+ // Update data-search with formatted date text
+ const dateText = lastCommSpan.textContent.trim()
+ lastCommCell.setAttribute('data-search', dateText)
+
+ // Refresh search after data-search update
+ performSearch()
+
+ lastCommSpan.classList.remove('cmd-updated')
+ void lastCommSpan.offsetWidth
+ lastCommSpan.classList.add('cmd-updated')
+
+ // Remove class after animation completes
+ setTimeout(() => lastCommSpan.classList.remove('cmd-updated'), 2000)
+ }
+ }
+ }
+ }
+ })
+ }
+
+ document.body.addEventListener('cmd::update', healthCmdUpdateHandler)
+}
+
+// ========================================
+// === HELPER FUNCTIONS ===
+// ========================================
+
+/**
+ * Format tooltip with command dates
+ * @param {string} label - Label for the command
+ * @param {Object} cmdData - Command data object
+ * @returns {string} Formatted tooltip text
+ */
+const formatTooltip = (label, cmdData) => {
+ if (!cmdData) {
+ return label
+ }
+
+ const valueDate = cmdData.valueDate || '-'
+ const collectDate = cmdData.collectDate || '-'
+
+ return `${label}
Date de valeur : ${valueDate} Date de collecte : ${collectDate} `
+}
+
+/**
+ * Format command value for display
+ * @param {Object} cmdData - Command data object
+ * @param {string} type - Type of command (optional, e.g., 'cron')
+ * @param {string} eqType - Equipment type (optional, e.g., 'local', 'distant')
+ * @param {Object} cronCustomData - Cron custom status data (optional)
+ * @returns {string} Formatted HTML
+ */
+const formatCmdValue = (cmdData, type = null, eqType = null, cronCustomData = null) => {
+ if (!cmdData || cmdData.value === null || cmdData.value === undefined || cmdData.value === '') {
+ return '
- '
+ }
+
+ const value = cmdData.value
+ const unit = cmdData.unit || ''
+
+ // Special handling for SSH Status
+ if (type === 'ssh') {
+ if (value === 'OK') {
+ return '
OK'
+ } else if (value === 'KO') {
+ return '
KO'
+ } else if (value === 'No') {
+ return '
- '
+ }
+ }
+
+ // Special handling for Cron Status
+ if (type === 'cron') {
+ const isOn = value === '1' || value === 1 || value === 'Yes'
+ const isCustom = cronCustomData && (cronCustomData.value === '1' || cronCustomData.value === 1)
+
+ // Custom ON = orange badge with play icon
+ if (isCustom && isOn) {
+ return '
ON'
+ }
+ // Custom OFF = orange badge with pause icon
+ else if (isCustom && !isOn) {
+ return '
OFF'
+ }
+ // Default ON = green badge with play icon
+ else if (isOn) {
+ return '
ON'
+ }
+ // Default OFF = red badge with pause icon
+ else {
+ return '
OFF'
+ }
+ }
+
+ // Format other special values
+ if (value === 'OK' || value === 'Running') {
+ return `
${value} `
+ } else if (value === 'KO' || value === 'Stopped') {
+ return `
${value} `
+ }
+
+ return `${value}${unit ? ' ' + unit : ''}`
+}
+
+/**
+ * Format date for display
+ * @param {string} dateStr - Date string to format
+ * @param {string} eqType - Equipment type ('local' or 'distant')
+ * @returns {string} Formatted date or dash if invalid
+ */
+const formatDate = (dateStr, eqType = null) => {
+ if (!dateStr || dateStr === '' || dateStr === '0000-00-00 00:00:00') {
+ return '
- '
+ }
+
+ try {
+ const date = new Date(dateStr)
+ if (isNaN(date.getTime())) {
+ return '
- '
+ }
+
+ const now = new Date()
+ const diffMs = now - date
+ const diffMins = Math.floor(diffMs / 60000)
+
+ // Color based on age and equipment type
+ const formattedDate = dateStr.slice(0, 19).replace('T', ' ')
+
+ // Green threshold varies: local <= 5min, distant <= 15min
+ const greenThreshold = (eqType === 'local') ? 5 : 15
+
+ if (diffMins <= greenThreshold) {
+ return `
${formattedDate} `
+ } else if (diffMins <= 30) {
+ return `
${formattedDate} `
+ } else {
+ return `
${formattedDate} `
+ }
+ } catch (e) {
+ return '
- '
+ }
+}
+
+// ========================================
+// === GLOBAL EXPOSURE ===
+// ========================================
+
+// Expose functions globally for modal to call
+window.initModalHealthMonitoring = initModalHealthMonitoring
+window.cleanupHealthMonitoring = cleanupHealthMonitoring
+
+})() // End of IIFE protection
+
diff --git a/desktop/modal/health.monitoring.php b/desktop/modal/health.monitoring.php
new file mode 100644
index 00000000..96bfda9e
--- /dev/null
+++ b/desktop/modal/health.monitoring.php
@@ -0,0 +1,152 @@
+.
+ */
+
+if (!isConnect()) {
+ throw new Exception('{{401 - Accès non autorisé}}');
+}
+
+?>
+
+
+
+
+
+
+
+
+ {{Résumé de l'état de santé de tous vos équipements Monitoring}}
+
+
+
+
+
+
+
+ {{Nom}}
+ {{Actif}}
+ {{Visible}}
+ {{Type}}
+ {{Hôte SSH}}
+ {{SSH Status}}
+ {{Cron Status}}
+ {{Uptime}}
+ {{Charge 1min}}
+ {{Charge 5min}}
+ {{Charge 15min}}
+ {{Adresse IP}}
+ {{Dernière Communication}}
+
+
+
+
+
+ {{Chargement des données...}}
+
+
+
+
+
+
+
+
+
diff --git a/desktop/php/Monitoring.php b/desktop/php/Monitoring.php
index 75c985da..4bcafa97 100644
--- a/desktop/php/Monitoring.php
+++ b/desktop/php/Monitoring.php
@@ -51,6 +51,11 @@
{{Configuration}}