Agregar temperatura Top-Rack
This commit is contained in:
@@ -248,6 +248,7 @@ Ubicacion fisica o logica. Actualmente se muestra en la tarjeta.
|
|||||||
Por SSH se leen datos de solo lectura:
|
Por SSH se leen datos de solo lectura:
|
||||||
|
|
||||||
- Temperatura CPU
|
- Temperatura CPU
|
||||||
|
- Temperatura Top-Rack opcional desde archivo JSON escrito por Node-RED
|
||||||
- Uso CPU
|
- Uso CPU
|
||||||
- Uso RAM
|
- Uso RAM
|
||||||
- Uso disco raiz `/`
|
- Uso disco raiz `/`
|
||||||
@@ -262,6 +263,26 @@ Por SSH se leen datos de solo lectura:
|
|||||||
- Velocidad nominal si existe
|
- Velocidad nominal si existe
|
||||||
- Trafico RX/TX calculado entre scans
|
- Trafico RX/TX calculado entre scans
|
||||||
|
|
||||||
|
### Temperatura Top-Rack
|
||||||
|
|
||||||
|
Si una RPi tiene un sensor externo de temperatura del rack, Node-RED puede escribir la lectura en un archivo local del mismo equipo:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"temperatureC": 23.7,
|
||||||
|
"sensor": "rack-top",
|
||||||
|
"updatedAt": "2026-06-05T21:12:18.997Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
En el configurador, coloca la ruta en el dispositivo que tiene el sensor:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/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.
|
||||||
|
|
||||||
## Clientes Activos
|
## Clientes Activos
|
||||||
|
|
||||||
El dashboard envia heartbeat al backend cuando esta visible.
|
El dashboard envia heartbeat al backend cuando esta visible.
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
"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",
|
||||||
@@ -55,6 +56,7 @@
|
|||||||
"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",
|
||||||
@@ -69,6 +71,7 @@
|
|||||||
"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",
|
||||||
@@ -83,6 +86,7 @@
|
|||||||
"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",
|
||||||
@@ -97,6 +101,7 @@
|
|||||||
"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",
|
||||||
@@ -111,6 +116,7 @@
|
|||||||
"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",
|
||||||
@@ -125,6 +131,7 @@
|
|||||||
"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",
|
||||||
@@ -139,6 +146,7 @@
|
|||||||
"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",
|
||||||
|
|||||||
+7
-2
@@ -233,6 +233,7 @@
|
|||||||
.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; }
|
||||||
@@ -445,7 +446,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 { grid-column: span 3; }
|
.field-name, .field-role, .field-host, .password-credential, .field-rack-temp { 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; }
|
||||||
@@ -460,7 +461,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-delete { grid-column: span 1; }
|
.credential-note, .field-rack-temp, .field-delete { grid-column: span 1; }
|
||||||
.threshold-row { grid-template-columns: 1fr; }
|
.threshold-row { grid-template-columns: 1fr; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -702,6 +703,9 @@
|
|||||||
<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>
|
||||||
@@ -848,6 +852,7 @@
|
|||||||
password: "",
|
password: "",
|
||||||
privateKeyPath: "",
|
privateKeyPath: "",
|
||||||
authMethod: "password",
|
authMethod: "password",
|
||||||
|
rackTemperatureFile: "",
|
||||||
model: "RPi 4",
|
model: "RPi 4",
|
||||||
role: "",
|
role: "",
|
||||||
location: "Rack",
|
location: "Rack",
|
||||||
|
|||||||
+9
-1
@@ -282,7 +282,7 @@
|
|||||||
|
|
||||||
.summary {
|
.summary {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(5, minmax(130px, 1fr));
|
grid-template-columns: repeat(6, minmax(120px, 1fr));
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -981,6 +981,14 @@
|
|||||||
hottestSummaryClass(summary.hottest.value, temperatureThresholds)
|
hottestSummaryClass(summary.hottest.value, temperatureThresholds)
|
||||||
)
|
)
|
||||||
: summaryItem("Mas caliente", "--"),
|
: summaryItem("Mas caliente", "--"),
|
||||||
|
summary.rackTemperature
|
||||||
|
? summaryItemWithNote(
|
||||||
|
"Temperatura Top-Rack",
|
||||||
|
`${summary.rackTemperature.temperatureC.toFixed(1)} C`,
|
||||||
|
summary.rackTemperature.deviceName || summary.rackTemperature.sensor || "",
|
||||||
|
hottestSummaryClass(summary.rackTemperature.temperatureC, temperatureThresholds)
|
||||||
|
)
|
||||||
|
: summaryItem("Temperatura Top-Rack", "--"),
|
||||||
summaryItem("Total RPi configuradas", summary.total || 0),
|
summaryItem("Total RPi configuradas", summary.total || 0),
|
||||||
summaryItem("Alertas", summary.alerts?.length || 0, summary.alerts?.length > 0 ? "summary-hot" : "")
|
summaryItem("Alertas", summary.alerts?.length || 0, summary.alerts?.length > 0 ? "summary-hot" : "")
|
||||||
].join("");
|
].join("");
|
||||||
|
|||||||
@@ -149,6 +149,16 @@ chmod 600 ~/.ssh/authorized_keys</code></pre>
|
|||||||
<pre><code>0 clientes visibles -> idleScanIntervalSeconds
|
<pre><code>0 clientes visibles -> idleScanIntervalSeconds
|
||||||
1+ clientes visibles -> refreshIntervalSeconds</code></pre>
|
1+ clientes visibles -> refreshIntervalSeconds</code></pre>
|
||||||
|
|
||||||
|
<h2>Temperatura Top-Rack</h2>
|
||||||
|
<p>Si una RPi tiene un sensor externo del rack, Node-RED puede escribir la lectura en un archivo JSON local.</p>
|
||||||
|
<pre><code>{
|
||||||
|
"temperatureC": 23.7,
|
||||||
|
"sensor": "rack-top",
|
||||||
|
"updatedAt": "2026-06-05T21:12:18.997Z"
|
||||||
|
}</code></pre>
|
||||||
|
<p>En el configurador, en ese dispositivo, usa <code>Archivo temp rack</code> con una ruta como:</p>
|
||||||
|
<pre><code>/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
|
||||||
docker compose up -d --build</code></pre>
|
docker compose up -d --build</code></pre>
|
||||||
|
|||||||
@@ -165,12 +165,22 @@ function mockMetrics(device) {
|
|||||||
speedMbps: device.model === "RPi 3" ? null : 1000,
|
speedMbps: device.model === "RPi 3" ? null : 1000,
|
||||||
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
|
||||||
|
? {
|
||||||
|
temperatureC: Number(seededNumber(`${device.id}:rack-temp`, 25, 37).toFixed(1)),
|
||||||
|
sensor: "rack-top",
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
ageSeconds: 0,
|
||||||
|
sourceFile: device.rackTemperatureFile
|
||||||
}
|
}
|
||||||
|
: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const REMOTE_METRICS_SCRIPT = `
|
const REMOTE_METRICS_SCRIPT = `
|
||||||
set +e
|
set +e
|
||||||
|
rack_temperature_file="$RACK_TEMPERATURE_FILE"
|
||||||
temp_raw="$(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null)"
|
temp_raw="$(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null)"
|
||||||
if [ -n "$temp_raw" ]; then
|
if [ -n "$temp_raw" ]; then
|
||||||
temp_c="$(awk "BEGIN { printf \\"%.1f\\", $temp_raw / 1000 }")"
|
temp_c="$(awk "BEGIN { printf \\"%.1f\\", $temp_raw / 1000 }")"
|
||||||
@@ -259,6 +269,27 @@ if [ "$ha_status" = "n/a" ] && [ "$docker_installed" = "1" ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
rack_temperature_c=""
|
||||||
|
rack_temperature_sensor=""
|
||||||
|
rack_temperature_updated_at=""
|
||||||
|
rack_temperature_age_seconds=""
|
||||||
|
if [ -n "$rack_temperature_file" ] && [ -r "$rack_temperature_file" ]; then
|
||||||
|
rack_payload="$(cat "$rack_temperature_file" 2>/dev/null)"
|
||||||
|
rack_temperature_c="$(printf '%s' "$rack_payload" | awk -F '[:,}]' '/temperatureC/ { gsub(/[^0-9.\\-]/, "", $2); print $2; exit }')"
|
||||||
|
if [ -z "$rack_temperature_c" ]; then
|
||||||
|
rack_temperature_c="$(printf '%s' "$rack_payload" | awk 'NR==1 { gsub(/[^0-9.\\-]/, "", $1); print $1; exit }')"
|
||||||
|
fi
|
||||||
|
rack_temperature_sensor="$(printf '%s' "$rack_payload" | sed -n 's/.*"sensor"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | head -n 1)"
|
||||||
|
rack_temperature_updated_at="$(printf '%s' "$rack_payload" | sed -n 's/.*"updatedAt"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | head -n 1)"
|
||||||
|
if [ -n "$rack_temperature_updated_at" ]; then
|
||||||
|
rack_updated_epoch="$(date -d "$rack_temperature_updated_at" +%s 2>/dev/null)"
|
||||||
|
now_epoch="$(date +%s 2>/dev/null)"
|
||||||
|
if [ -n "$rack_updated_epoch" ] && [ -n "$now_epoch" ] && [ "$now_epoch" -ge "$rack_updated_epoch" ]; then
|
||||||
|
rack_temperature_age_seconds="$((now_epoch - rack_updated_epoch))"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
printf 'cpuTempC=%s\\n' "$temp_c"
|
printf 'cpuTempC=%s\\n' "$temp_c"
|
||||||
printf 'cpuCores=%s\\n' "$cpu_cores"
|
printf 'cpuCores=%s\\n' "$cpu_cores"
|
||||||
printf 'cpuUsagePercent=%s\\n' "$cpu_usage"
|
printf 'cpuUsagePercent=%s\\n' "$cpu_usage"
|
||||||
@@ -283,6 +314,10 @@ printf 'dockerRunning=%s\\n' "$docker_running"
|
|||||||
printf 'dockerStopped=%s\\n' "$docker_stopped"
|
printf 'dockerStopped=%s\\n' "$docker_stopped"
|
||||||
printf 'dockerUnhealthy=%s\\n' "$docker_unhealthy"
|
printf 'dockerUnhealthy=%s\\n' "$docker_unhealthy"
|
||||||
printf 'homeAssistantStatus=%s\\n' "$ha_status"
|
printf 'homeAssistantStatus=%s\\n' "$ha_status"
|
||||||
|
printf 'rackTemperatureC=%s\\n' "$rack_temperature_c"
|
||||||
|
printf 'rackTemperatureSensor=%s\\n' "$rack_temperature_sensor"
|
||||||
|
printf 'rackTemperatureUpdatedAt=%s\\n' "$rack_temperature_updated_at"
|
||||||
|
printf 'rackTemperatureAgeSeconds=%s\\n' "$rack_temperature_age_seconds"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function parseKeyValueOutput(output) {
|
function parseKeyValueOutput(output) {
|
||||||
@@ -301,6 +336,10 @@ function toNumber(value, fallback = 0) {
|
|||||||
return Number.isFinite(parsed) ? parsed : fallback;
|
return Number.isFinite(parsed) ? parsed : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shellQuote(value) {
|
||||||
|
return `'${String(value || "").replace(/'/g, `'\\''`)}'`;
|
||||||
|
}
|
||||||
|
|
||||||
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();
|
||||||
@@ -333,9 +372,12 @@ function networkRates(device, values) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function runCommandWithInput(command, args, input, timeoutMs) {
|
function runCommandWithInput(command, args, input, timeoutMs, env = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
|
const child = spawn(command, args, {
|
||||||
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
env: { ...process.env, ...env }
|
||||||
|
});
|
||||||
let stdout = "";
|
let stdout = "";
|
||||||
let stderr = "";
|
let stderr = "";
|
||||||
let settled = false;
|
let settled = false;
|
||||||
@@ -428,9 +470,11 @@ async function sshMetrics(device) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { stdout } = await runCommandWithInput(command, args, REMOTE_METRICS_SCRIPT, timeoutMs);
|
const remoteScript = `RACK_TEMPERATURE_FILE=${shellQuote(device.rackTemperatureFile || "")}\n${REMOTE_METRICS_SCRIPT}`;
|
||||||
|
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);
|
||||||
|
const rackTemperatureC = toNumber(values.rackTemperatureC, null);
|
||||||
const loadAverage = String(values.loadAverage || "")
|
const loadAverage = String(values.loadAverage || "")
|
||||||
.split(/\s+/)
|
.split(/\s+/)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
@@ -460,7 +504,16 @@ async function sshMetrics(device) {
|
|||||||
detected: values.homeAssistantStatus && values.homeAssistantStatus !== "n/a",
|
detected: values.homeAssistantStatus && values.homeAssistantStatus !== "n/a",
|
||||||
status: values.homeAssistantStatus || "n/a"
|
status: values.homeAssistantStatus || "n/a"
|
||||||
},
|
},
|
||||||
network: networkRates(device, values)
|
network: networkRates(device, values),
|
||||||
|
rackTemperature: Number.isFinite(rackTemperatureC)
|
||||||
|
? {
|
||||||
|
temperatureC: Number(rackTemperatureC.toFixed(1)),
|
||||||
|
sensor: values.rackTemperatureSensor || "",
|
||||||
|
updatedAt: values.rackTemperatureUpdatedAt || "",
|
||||||
|
ageSeconds: toNumber(values.rackTemperatureAgeSeconds, null),
|
||||||
|
sourceFile: device.rackTemperatureFile || ""
|
||||||
|
}
|
||||||
|
: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,6 +573,15 @@ function buildSummary(devices) {
|
|||||||
.filter((device) => device.metrics && Number.isFinite(device.metrics.cpuTempC))
|
.filter((device) => device.metrics && Number.isFinite(device.metrics.cpuTempC))
|
||||||
.map((device) => ({ name: device.name, value: device.metrics.cpuTempC }));
|
.map((device) => ({ name: device.name, value: device.metrics.cpuTempC }));
|
||||||
const hottest = temps.sort((a, b) => b.value - a.value)[0] || null;
|
const hottest = temps.sort((a, b) => b.value - a.value)[0] || null;
|
||||||
|
const rackTemperature = online
|
||||||
|
.map((device) => {
|
||||||
|
const rack = device.metrics?.rackTemperature;
|
||||||
|
return rack && Number.isFinite(rack.temperatureC)
|
||||||
|
? { ...rack, deviceName: device.name }
|
||||||
|
: null;
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort((a, b) => (a.ageSeconds ?? Number.MAX_SAFE_INTEGER) - (b.ageSeconds ?? Number.MAX_SAFE_INTEGER))[0] || null;
|
||||||
const alerts = [];
|
const alerts = [];
|
||||||
|
|
||||||
for (const device of devices) {
|
for (const device of devices) {
|
||||||
@@ -537,6 +599,7 @@ function buildSummary(devices) {
|
|||||||
online: online.length,
|
online: online.length,
|
||||||
offline: active.length - online.length,
|
offline: active.length - online.length,
|
||||||
hottest,
|
hottest,
|
||||||
|
rackTemperature,
|
||||||
alerts
|
alerts
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user