Configurar Top-Rack como ajuste global

This commit is contained in:
2026-06-05 23:23:30 +02:00
parent 5eb7964a7b
commit c4af121597
5 changed files with 68 additions and 23 deletions
+10 -2
View File
@@ -275,10 +275,18 @@ Si una RPi tiene un sensor externo de temperatura del rack, Node-RED puede escri
} }
``` ```
En el configurador, coloca la ruta en el dispositivo que tiene el sensor: En el configurador, usa la seccion general `Temperatura Top-Rack`:
```text ```text
/home/yamaray/monitor-rpi/rack-temp.json RPi origen: RPi - MeshCore · 192.168.0.60
Path JSON temperatura: /home/yamaray/monitor-rpi/rack-temp.json
```
La configuracion se guarda como:
```text
rackTemperatureSourceDeviceId: rpi3-meshcore
rackTemperatureFile: /home/yamaray/monitor-rpi/rack-temp.json
``` ```
El backend lee ese archivo durante el mismo scan SSH y muestra el valor como `Temperatura Top-Rack` en la primera linea del dashboard. El backend lee ese archivo durante el mismo scan SSH y muestra el valor como `Temperatura Top-Rack` en la primera linea del dashboard.
+2 -8
View File
@@ -5,6 +5,8 @@
"sshTimeoutSeconds": 8, "sshTimeoutSeconds": 8,
"sshPrivateKeyPath": "/ssh/monitor_rpi_ed25519", "sshPrivateKeyPath": "/ssh/monitor_rpi_ed25519",
"sshPublicKeyPath": "/ssh/monitor_rpi_ed25519.pub", "sshPublicKeyPath": "/ssh/monitor_rpi_ed25519.pub",
"rackTemperatureSourceDeviceId": "rpi3-meshcore",
"rackTemperatureFile": "/home/yamaray/monitor-rpi/rack-temp.json",
"temperatureThresholdsC": { "temperatureThresholdsC": {
"warning": 60, "warning": 60,
"hot": 70, "hot": 70,
@@ -41,7 +43,6 @@
"password": "", "password": "",
"authMethod": "password", "authMethod": "password",
"privateKeyPath": "", "privateKeyPath": "",
"rackTemperatureFile": "",
"role": "Home Assistant Local", "role": "Home Assistant Local",
"port": 22, "port": 22,
"id": "rpi5-ha-main", "id": "rpi5-ha-main",
@@ -56,7 +57,6 @@
"password": "", "password": "",
"authMethod": "password", "authMethod": "password",
"privateKeyPath": "", "privateKeyPath": "",
"rackTemperatureFile": "",
"role": "Home Assistant Cabaña", "role": "Home Assistant Cabaña",
"port": 22, "port": 22,
"id": "rpi4-ha-second", "id": "rpi4-ha-second",
@@ -71,7 +71,6 @@
"password": "", "password": "",
"authMethod": "password", "authMethod": "password",
"privateKeyPath": "", "privateKeyPath": "",
"rackTemperatureFile": "",
"role": "Docker", "role": "Docker",
"port": 22, "port": 22,
"id": "rpi5-jose-docker", "id": "rpi5-jose-docker",
@@ -86,7 +85,6 @@
"password": "", "password": "",
"authMethod": "password", "authMethod": "password",
"privateKeyPath": "", "privateKeyPath": "",
"rackTemperatureFile": "",
"role": "Docker - Daniel", "role": "Docker - Daniel",
"port": 22, "port": 22,
"id": "rpi5-dani-docker", "id": "rpi5-dani-docker",
@@ -101,7 +99,6 @@
"password": "", "password": "",
"authMethod": "password", "authMethod": "password",
"privateKeyPath": "", "privateKeyPath": "",
"rackTemperatureFile": "",
"role": "nginx - wireguard ", "role": "nginx - wireguard ",
"port": 22, "port": 22,
"id": "rpi3-nginx", "id": "rpi3-nginx",
@@ -116,7 +113,6 @@
"password": "", "password": "",
"authMethod": "password", "authMethod": "password",
"privateKeyPath": "", "privateKeyPath": "",
"rackTemperatureFile": "",
"role": "MQTT - varios", "role": "MQTT - varios",
"port": 22, "port": 22,
"id": "rpi3-pi3home", "id": "rpi3-pi3home",
@@ -131,7 +127,6 @@
"password": "", "password": "",
"authMethod": "password", "authMethod": "password",
"privateKeyPath": "", "privateKeyPath": "",
"rackTemperatureFile": "/home/yamaray/monitor-rpi/rack-temp.json",
"role": "Meshcore-Interface", "role": "Meshcore-Interface",
"port": 22, "port": 22,
"id": "rpi3-meshcore", "id": "rpi3-meshcore",
@@ -146,7 +141,6 @@
"password": "", "password": "",
"authMethod": "password", "authMethod": "password",
"privateKeyPath": "", "privateKeyPath": "",
"rackTemperatureFile": "",
"role": "Reserva", "role": "Reserva",
"port": 22, "port": 22,
"id": "rpi3-node-3", "id": "rpi3-node-3",
+25 -7
View File
@@ -233,7 +233,6 @@
.field-role { grid-column: span 3; } .field-role { grid-column: span 3; }
.field-model { grid-column: span 2; } .field-model { grid-column: span 2; }
.field-location { grid-column: span 2; } .field-location { grid-column: span 2; }
.field-rack-temp { grid-column: span 4; }
.field-host { grid-column: span 3; } .field-host { grid-column: span 3; }
.field-port { grid-column: span 1; } .field-port { grid-column: span 1; }
.field-username { grid-column: span 2; } .field-username { grid-column: span 2; }
@@ -446,7 +445,7 @@
@media (max-width: 1180px) { @media (max-width: 1180px) {
.device { grid-template-columns: repeat(6, minmax(0, 1fr)); align-items: start; } .device { grid-template-columns: repeat(6, minmax(0, 1fr)); align-items: start; }
.field-name, .field-role, .field-host, .password-credential, .field-rack-temp { grid-column: span 3; } .field-name, .field-role, .field-host, .password-credential { grid-column: span 3; }
.field-id, .field-model, .field-location, .field-username, .field-auth, .field-active, .field-delete { grid-column: span 2; } .field-id, .field-model, .field-location, .field-username, .field-auth, .field-active, .field-delete { grid-column: span 2; }
.field-port { grid-column: span 1; } .field-port { grid-column: span 1; }
.credential-note { grid-column: span 4; } .credential-note { grid-column: span 4; }
@@ -461,7 +460,7 @@
.top-grid, .device { grid-template-columns: 1fr; } .top-grid, .device { grid-template-columns: 1fr; }
.field-active, .field-name, .field-id, .field-role, .field-model, .field-location, .field-active, .field-name, .field-id, .field-role, .field-model, .field-location,
.field-host, .field-port, .field-username, .field-auth, .password-credential, .field-host, .field-port, .field-username, .field-auth, .password-credential,
.credential-note, .field-rack-temp, .field-delete { grid-column: span 1; } .credential-note, .field-delete { grid-column: span 1; }
.threshold-row { grid-template-columns: 1fr; } .threshold-row { grid-template-columns: 1fr; }
} }
</style> </style>
@@ -503,6 +502,17 @@
<input id="sshTimeoutSeconds" type="number" min="2" step="1"> <input id="sshTimeoutSeconds" type="number" min="2" step="1">
</label> </label>
</div> </div>
<div class="ssh-key-panel">
<h2>Temperatura Top-Rack</h2>
<div class="ssh-key-fields">
<label title="RPi desde el que el monitor leera el archivo JSON de temperatura. Se muestra nombre e IP para identificarlo rapido.">RPi origen
<select id="rackTemperatureSourceDeviceId"></select>
</label>
<label title="Path del archivo JSON en el RPi origen. Node-RED debe escribir ahi la ultima temperatura.">Path JSON temperatura
<input id="rackTemperatureFile" placeholder="/home/yamaray/monitor-rpi/rack-temp.json">
</label>
</div>
</div>
<div class="ssh-key-panel"> <div class="ssh-key-panel">
<h2>Clave SSH del monitor</h2> <h2>Clave SSH del monitor</h2>
<div class="ssh-key-fields"> <div class="ssh-key-fields">
@@ -703,9 +713,6 @@
<div class="credential-note"> <div class="credential-note">
Metodo activo: <strong>${authMethod === "key" ? "Clave asimetrica: copiar clave publica del monitor en la RPi" : "Password"}</strong> Metodo activo: <strong>${authMethod === "key" ? "Clave asimetrica: copiar clave publica del monitor en la RPi" : "Password"}</strong>
</div> </div>
<label class="field-rack-temp" title="Archivo opcional en este RPi con la temperatura del rack escrita por Node-RED. Si esta vacio, no se recoge temperatura Top-Rack desde este dispositivo.">Archivo temp rack
<input data-field="rackTemperatureFile" value="${device.rackTemperatureFile || ""}" placeholder="/home/yamaray/monitor-rpi/rack-temp.json">
</label>
<label class="field-active" title="Si esta activo, este dispositivo se consulta por SSH. Si esta inactivo, se muestra deshabilitado y no se escanea.">Activo <label class="field-active" title="Si esta activo, este dispositivo se consulta por SSH. Si esta inactivo, se muestra deshabilitado y no se escanea.">Activo
<input data-field="active" type="checkbox" ${device.active ? "checked" : ""}> <input data-field="active" type="checkbox" ${device.active ? "checked" : ""}>
</label> </label>
@@ -720,6 +727,16 @@
document.querySelector("#refreshIntervalSeconds").value = state.config.refreshIntervalSeconds || 30; document.querySelector("#refreshIntervalSeconds").value = state.config.refreshIntervalSeconds || 30;
document.querySelector("#idleScanIntervalSeconds").value = state.config.idleScanIntervalSeconds || 300; document.querySelector("#idleScanIntervalSeconds").value = state.config.idleScanIntervalSeconds || 300;
document.querySelector("#sshTimeoutSeconds").value = state.config.sshTimeoutSeconds || 8; document.querySelector("#sshTimeoutSeconds").value = state.config.sshTimeoutSeconds || 8;
const rackSource = document.querySelector("#rackTemperatureSourceDeviceId");
rackSource.innerHTML = [
`<option value="">Sin sensor Top-Rack</option>`,
...state.config.devices.map((device) => {
const value = device.id || device.host || device.name || "";
const label = `${device.name || value} · ${device.host || "sin IP"}`;
return `<option value="${value}" ${state.config.rackTemperatureSourceDeviceId === value ? "selected" : ""}>${label}</option>`;
})
].join("");
document.querySelector("#rackTemperatureFile").value = state.config.rackTemperatureFile || "";
document.querySelector("#sshPrivateKeyPath").value = state.config.sshPrivateKeyPath || "/ssh/monitor_rpi_ed25519"; document.querySelector("#sshPrivateKeyPath").value = state.config.sshPrivateKeyPath || "/ssh/monitor_rpi_ed25519";
document.querySelector("#sshPublicKeyPath").value = state.config.sshPublicKeyPath || `${document.querySelector("#sshPrivateKeyPath").value}.pub`; document.querySelector("#sshPublicKeyPath").value = state.config.sshPublicKeyPath || `${document.querySelector("#sshPrivateKeyPath").value}.pub`;
document.querySelector("#warningTemp").value = state.config.temperatureThresholdsC?.warning || 60; document.querySelector("#warningTemp").value = state.config.temperatureThresholdsC?.warning || 60;
@@ -745,6 +762,8 @@
state.config.refreshIntervalSeconds = Number(document.querySelector("#refreshIntervalSeconds").value || 30); state.config.refreshIntervalSeconds = Number(document.querySelector("#refreshIntervalSeconds").value || 30);
state.config.idleScanIntervalSeconds = Number(document.querySelector("#idleScanIntervalSeconds").value || 300); state.config.idleScanIntervalSeconds = Number(document.querySelector("#idleScanIntervalSeconds").value || 300);
state.config.sshTimeoutSeconds = Number(document.querySelector("#sshTimeoutSeconds").value || 8); state.config.sshTimeoutSeconds = Number(document.querySelector("#sshTimeoutSeconds").value || 8);
state.config.rackTemperatureSourceDeviceId = document.querySelector("#rackTemperatureSourceDeviceId").value || "";
state.config.rackTemperatureFile = document.querySelector("#rackTemperatureFile").value || "";
state.config.sshPrivateKeyPath = document.querySelector("#sshPrivateKeyPath").value || "/ssh/monitor_rpi_ed25519"; state.config.sshPrivateKeyPath = document.querySelector("#sshPrivateKeyPath").value || "/ssh/monitor_rpi_ed25519";
state.config.sshPublicKeyPath = document.querySelector("#sshPublicKeyPath").value || `${state.config.sshPrivateKeyPath}.pub`; state.config.sshPublicKeyPath = document.querySelector("#sshPublicKeyPath").value || `${state.config.sshPrivateKeyPath}.pub`;
state.config.temperatureThresholdsC = { state.config.temperatureThresholdsC = {
@@ -852,7 +871,6 @@
password: "", password: "",
privateKeyPath: "", privateKeyPath: "",
authMethod: "password", authMethod: "password",
rackTemperatureFile: "",
model: "RPi 4", model: "RPi 4",
role: "", role: "",
location: "Rack", location: "Rack",
+3 -2
View File
@@ -156,8 +156,9 @@ chmod 600 ~/.ssh/authorized_keys</code></pre>
"sensor": "rack-top", "sensor": "rack-top",
"updatedAt": "2026-06-05T21:12:18.997Z" "updatedAt": "2026-06-05T21:12:18.997Z"
}</code></pre> }</code></pre>
<p>En el configurador, en ese dispositivo, usa <code>Archivo temp rack</code> con una ruta como:</p> <p>En el configurador, usa la seccion general <code>Temperatura Top-Rack</code>:</p>
<pre><code>/home/yamaray/monitor-rpi/rack-temp.json</code></pre> <pre><code>RPi origen: RPi - MeshCore · 192.168.0.60
Path JSON temperatura: /home/yamaray/monitor-rpi/rack-temp.json</code></pre>
<h2>Docker</h2> <h2>Docker</h2>
<pre><code>cd /home/yamaray/docker/monitorRPi <pre><code>cd /home/yamaray/docker/monitorRPi
+28 -4
View File
@@ -49,6 +49,7 @@ async function readConfig() {
parsed.temperatureThresholdsC = normalizeTemperatureThresholds(parsed.temperatureThresholdsC); parsed.temperatureThresholdsC = normalizeTemperatureThresholds(parsed.temperatureThresholdsC);
parsed.metricThresholdsPercent = normalizeMetricThresholds(parsed.metricThresholdsPercent); parsed.metricThresholdsPercent = normalizeMetricThresholds(parsed.metricThresholdsPercent);
parsed.devices = Array.isArray(parsed.devices) ? parsed.devices.map(normalizeDeviceConfig) : []; parsed.devices = Array.isArray(parsed.devices) ? parsed.devices.map(normalizeDeviceConfig) : [];
normalizeRackTemperatureConfig(parsed);
return parsed; return parsed;
} }
@@ -65,6 +66,7 @@ async function writeConfig(nextConfig) {
metricThresholdsPercent: normalizeMetricThresholds(nextConfig.metricThresholdsPercent), metricThresholdsPercent: normalizeMetricThresholds(nextConfig.metricThresholdsPercent),
devices: Array.isArray(nextConfig.devices) ? nextConfig.devices.map(normalizeDeviceConfig) : [] devices: Array.isArray(nextConfig.devices) ? nextConfig.devices.map(normalizeDeviceConfig) : []
}; };
normalizeRackTemperatureConfig(normalized);
await fs.mkdir(path.dirname(CONFIG_PATH), { recursive: true }); await fs.mkdir(path.dirname(CONFIG_PATH), { recursive: true });
await fs.writeFile(CONFIG_PATH, JSON.stringify(normalized, null, 2) + "\n", "utf8"); await fs.writeFile(CONFIG_PATH, JSON.stringify(normalized, null, 2) + "\n", "utf8");
config = normalized; config = normalized;
@@ -80,6 +82,19 @@ function normalizeDeviceConfig(device = {}) {
}; };
} }
function normalizeRackTemperatureConfig(nextConfig) {
nextConfig.rackTemperatureSourceDeviceId = String(nextConfig.rackTemperatureSourceDeviceId || "");
nextConfig.rackTemperatureFile = String(nextConfig.rackTemperatureFile || "");
if (!nextConfig.rackTemperatureSourceDeviceId || !nextConfig.rackTemperatureFile) {
const legacyDevice = nextConfig.devices.find((device) => device.rackTemperatureFile);
if (legacyDevice) {
nextConfig.rackTemperatureSourceDeviceId = legacyDevice.id || "";
nextConfig.rackTemperatureFile = legacyDevice.rackTemperatureFile || "";
}
}
}
function classifyTemp(tempC) { function classifyTemp(tempC) {
const thresholds = config.temperatureThresholdsC || {}; const thresholds = config.temperatureThresholdsC || {};
if (tempC >= (thresholds.critical || 80)) return "critical"; if (tempC >= (thresholds.critical || 80)) return "critical";
@@ -130,6 +145,7 @@ function mockMetrics(device) {
const temp = seededNumber(`${device.id}:temp`, baseTemp - 4, baseTemp + 18); const temp = seededNumber(`${device.id}:temp`, baseTemp - 4, baseTemp + 18);
const dockerRole = /docker|gitea|nginx/i.test(device.role || ""); const dockerRole = /docker|gitea|nginx/i.test(device.role || "");
const haRole = /home assistant/i.test(device.role || ""); const haRole = /home assistant/i.test(device.role || "");
const rackTemperatureFile = rackTemperatureFileForDevice(device);
return { return {
cpuTempC: Number(temp.toFixed(1)), cpuTempC: Number(temp.toFixed(1)),
@@ -166,13 +182,13 @@ function mockMetrics(device) {
rxBytesPerSecond: Math.round(seededNumber(`${device.id}:rx`, 1200, 95000)), rxBytesPerSecond: Math.round(seededNumber(`${device.id}:rx`, 1200, 95000)),
txBytesPerSecond: Math.round(seededNumber(`${device.id}:tx`, 700, 55000)) txBytesPerSecond: Math.round(seededNumber(`${device.id}:tx`, 700, 55000))
}, },
rackTemperature: device.rackTemperatureFile rackTemperature: rackTemperatureFile
? { ? {
temperatureC: Number(seededNumber(`${device.id}:rack-temp`, 25, 37).toFixed(1)), temperatureC: Number(seededNumber(`${device.id}:rack-temp`, 25, 37).toFixed(1)),
sensor: "rack-top", sensor: "rack-top",
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
ageSeconds: 0, ageSeconds: 0,
sourceFile: device.rackTemperatureFile sourceFile: rackTemperatureFile
} }
: null : null
}; };
@@ -340,6 +356,13 @@ function shellQuote(value) {
return `'${String(value || "").replace(/'/g, `'\\''`)}'`; return `'${String(value || "").replace(/'/g, `'\\''`)}'`;
} }
function rackTemperatureFileForDevice(device) {
const sourceId = config.rackTemperatureSourceDeviceId || "";
const sourceMatches = sourceId && [device.id, device.host, device.name].includes(sourceId);
if (sourceMatches) return config.rackTemperatureFile || "";
return device.rackTemperatureFile || "";
}
function networkRates(device, values) { function networkRates(device, values) {
const sampleKey = device.id || `${device.host}:${device.port || 22}`; const sampleKey = device.id || `${device.host}:${device.port || 22}`;
const now = Date.now(); const now = Date.now();
@@ -470,7 +493,8 @@ async function sshMetrics(device) {
]; ];
} }
const remoteScript = `RACK_TEMPERATURE_FILE=${shellQuote(device.rackTemperatureFile || "")}\n${REMOTE_METRICS_SCRIPT}`; const rackTemperatureFile = rackTemperatureFileForDevice(device);
const remoteScript = `RACK_TEMPERATURE_FILE=${shellQuote(rackTemperatureFile)}\n${REMOTE_METRICS_SCRIPT}`;
const { stdout } = await runCommandWithInput(command, args, remoteScript, timeoutMs); const { stdout } = await runCommandWithInput(command, args, remoteScript, timeoutMs);
const values = parseKeyValueOutput(stdout); const values = parseKeyValueOutput(stdout);
const temp = toNumber(values.cpuTempC, 0); const temp = toNumber(values.cpuTempC, 0);
@@ -511,7 +535,7 @@ async function sshMetrics(device) {
sensor: values.rackTemperatureSensor || "", sensor: values.rackTemperatureSensor || "",
updatedAt: values.rackTemperatureUpdatedAt || "", updatedAt: values.rackTemperatureUpdatedAt || "",
ageSeconds: toNumber(values.rackTemperatureAgeSeconds, null), ageSeconds: toNumber(values.rackTemperatureAgeSeconds, null),
sourceFile: device.rackTemperatureFile || "" sourceFile: rackTemperatureFile
} }
: null : null
}; };