|
|
|
|
@ -13,13 +13,17 @@ let dashboardUpdateInProgress = false;
|
|
|
|
|
const stateTranslations = {
|
|
|
|
|
"NORMAL": "NORMAL",
|
|
|
|
|
"OFFLINE": "DESCONECTADO",
|
|
|
|
|
"DISABLED": "DESHABILITADO",
|
|
|
|
|
"WARNING": "ADVERTENCIA",
|
|
|
|
|
"CRITICAL": "CRÍTICO"
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let currentEnabledSensorIds = new Set(["temperature", "ph", "do", "ec"]);
|
|
|
|
|
|
|
|
|
|
const sensors = [
|
|
|
|
|
{
|
|
|
|
|
id: "temperature",
|
|
|
|
|
sensorId: "temperature",
|
|
|
|
|
name: "Temperatura",
|
|
|
|
|
file: "../data/EZORTD.json",
|
|
|
|
|
key: "temperature",
|
|
|
|
|
@ -73,6 +77,7 @@ const historicalCharts = [
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "ph-history",
|
|
|
|
|
sensorId: "ph",
|
|
|
|
|
title: "pH vs Tiempo",
|
|
|
|
|
file: "../logs/ph.csv",
|
|
|
|
|
valueKey: "ph",
|
|
|
|
|
@ -83,6 +88,7 @@ const historicalCharts = [
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "do-history",
|
|
|
|
|
sensorId: "do",
|
|
|
|
|
title: "Oxígeno Disuelto vs Tiempo",
|
|
|
|
|
file: "../logs/do.csv",
|
|
|
|
|
valueKey: "do",
|
|
|
|
|
@ -93,6 +99,7 @@ const historicalCharts = [
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "ec-history",
|
|
|
|
|
sensorId: "ec",
|
|
|
|
|
title: "Conductividad vs Tiempo",
|
|
|
|
|
file: "../logs/ec.csv",
|
|
|
|
|
valueKey: "ec",
|
|
|
|
|
@ -106,6 +113,16 @@ const historicalCharts = [
|
|
|
|
|
const chartInstances = new Map();
|
|
|
|
|
|
|
|
|
|
async function readSensor(sensor) {
|
|
|
|
|
if (!currentEnabledSensorIds.has(sensor.id)) {
|
|
|
|
|
return {
|
|
|
|
|
...sensor,
|
|
|
|
|
disabled: true,
|
|
|
|
|
online: false,
|
|
|
|
|
numericValue: null,
|
|
|
|
|
value: "--"
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`${sensor.file}?t=${Date.now()}`, {
|
|
|
|
|
cache: "no-store"
|
|
|
|
|
@ -138,6 +155,7 @@ async function readSensor(sensor) {
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...sensor,
|
|
|
|
|
disabled: false,
|
|
|
|
|
online: true,
|
|
|
|
|
numericValue: rawValue,
|
|
|
|
|
timestamp: data.timestamp || null,
|
|
|
|
|
@ -148,6 +166,7 @@ async function readSensor(sensor) {
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...sensor,
|
|
|
|
|
disabled: false,
|
|
|
|
|
online: false,
|
|
|
|
|
numericValue: null,
|
|
|
|
|
value: "DESCONECTADO"
|
|
|
|
|
@ -166,25 +185,27 @@ function setSensorState(result) {
|
|
|
|
|
// Traducir el estado para mostrar en pantalla, manteniendo la clase en inglés
|
|
|
|
|
stateElement.textContent = stateTranslations[state] || state;
|
|
|
|
|
|
|
|
|
|
["online", "normal", "warning", "critical", "offline"].forEach((className) => {
|
|
|
|
|
["online", "normal", "warning", "critical", "offline", "disabled"].forEach((className) => {
|
|
|
|
|
stateElement.classList.toggle(className, className === stateClass);
|
|
|
|
|
cardElement.classList.toggle(className, className === stateClass);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderSystemStatus(results) {
|
|
|
|
|
const activeSensors = results.filter((result) => result.online).length;
|
|
|
|
|
const offlineSensors = results.length - activeSensors;
|
|
|
|
|
const enabledResults = results.filter((result) => !result.disabled);
|
|
|
|
|
const activeSensors = enabledResults.filter((result) => result.online).length;
|
|
|
|
|
const offlineSensors = enabledResults.length - activeSensors;
|
|
|
|
|
const criticalSensors = results.filter((result) => result.alarmState === "CRITICAL").length;
|
|
|
|
|
const warningSensors = results.filter((result) => result.alarmState === "WARNING").length;
|
|
|
|
|
const allNormal = results.every((result) => result.alarmState === "NORMAL");
|
|
|
|
|
const allNormal = enabledResults.length > 0 &&
|
|
|
|
|
enabledResults.every((result) => result.alarmState === "NORMAL");
|
|
|
|
|
const anyOnline = activeSensors > 0;
|
|
|
|
|
const overallDot = document.getElementById("overall-dot");
|
|
|
|
|
const overallStatus = document.getElementById("overall-status");
|
|
|
|
|
const healthList = document.getElementById("sensor-health");
|
|
|
|
|
|
|
|
|
|
document.getElementById("active-count").textContent =
|
|
|
|
|
`${activeSensors} / ${results.length}`;
|
|
|
|
|
`${activeSensors} / ${enabledResults.length}`;
|
|
|
|
|
document.getElementById("offline-count").textContent = String(offlineSensors);
|
|
|
|
|
document.getElementById("last-update").textContent =
|
|
|
|
|
new Date().toLocaleString();
|
|
|
|
|
@ -230,10 +251,12 @@ async function updateDashboard() {
|
|
|
|
|
dashboardUpdateInProgress = true;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const [results, alarmConfig] = await Promise.all([
|
|
|
|
|
Promise.all(sensors.map(readSensor)),
|
|
|
|
|
const [runtimeConfig, alarmConfig] = await Promise.all([
|
|
|
|
|
readRuntimeConfig(),
|
|
|
|
|
readAlarmConfig()
|
|
|
|
|
]);
|
|
|
|
|
currentEnabledSensorIds = new Set(runtimeConfig.enabledSensors);
|
|
|
|
|
const results = await Promise.all(sensors.map(readSensor));
|
|
|
|
|
const evaluatedResults = results.map((result) =>
|
|
|
|
|
evaluateSensorAlarm(result, alarmConfig.thresholds)
|
|
|
|
|
);
|
|
|
|
|
@ -246,6 +269,25 @@ async function updateDashboard() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function readRuntimeConfig() {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch('/api/config/runtime', { cache: 'no-store' });
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
|
|
|
|
|
if (!response.ok || !Array.isArray(result.enabledSensors)) {
|
|
|
|
|
throw new Error("Runtime configuration unavailable");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn("No se pudo cargar la configuracion de sensores:", error);
|
|
|
|
|
return {
|
|
|
|
|
rate: currentLoggingRateSeconds,
|
|
|
|
|
enabledSensors: [...currentEnabledSensorIds]
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function readAlarmConfig() {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`${ALARM_CONFIG_FILE}?t=${Date.now()}`, {
|
|
|
|
|
@ -273,6 +315,14 @@ async function readAlarmConfig() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function evaluateSensorAlarm(sensorResult, thresholds) {
|
|
|
|
|
if (sensorResult.disabled) {
|
|
|
|
|
return {
|
|
|
|
|
...sensorResult,
|
|
|
|
|
alarmState: "DISABLED",
|
|
|
|
|
alarmMessage: "Sensor no habilitado en la configuracion actual"
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!sensorResult.online) {
|
|
|
|
|
return {
|
|
|
|
|
...sensorResult,
|
|
|
|
|
@ -342,7 +392,7 @@ function buildAlarmMessage(sensorResult, limits, alarmType) {
|
|
|
|
|
|
|
|
|
|
function getAlarmEvents(results) {
|
|
|
|
|
return results
|
|
|
|
|
.filter((result) => result.alarmState !== "NORMAL")
|
|
|
|
|
.filter((result) => !["NORMAL", "DISABLED"].includes(result.alarmState))
|
|
|
|
|
.map((result) => ({
|
|
|
|
|
sensorId: result.id,
|
|
|
|
|
sensorName: result.name,
|
|
|
|
|
@ -509,13 +559,28 @@ function formatTimestamp(rawTimestamp) {
|
|
|
|
|
return rawTimestamp;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setChartAvailability(chartConfig, hasData) {
|
|
|
|
|
function setChartAvailability(chartConfig, hasData, message = "No hay datos históricos disponibles") {
|
|
|
|
|
const emptyElement = document.getElementById(chartConfig.emptyElement);
|
|
|
|
|
const frameElement = emptyElement.closest(".chart-frame");
|
|
|
|
|
|
|
|
|
|
emptyElement.textContent = message;
|
|
|
|
|
frameElement.classList.toggle("empty", !hasData);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearHistoricalChart(chartConfig, message) {
|
|
|
|
|
const existingChart = chartInstances.get(chartConfig.id);
|
|
|
|
|
|
|
|
|
|
if (existingChart) {
|
|
|
|
|
existingChart.data.labels = [];
|
|
|
|
|
existingChart.data.datasets.forEach((dataset) => {
|
|
|
|
|
dataset.data = [];
|
|
|
|
|
});
|
|
|
|
|
existingChart.update("none");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setChartAvailability(chartConfig, false, message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildChartDataset(chartConfig, points) {
|
|
|
|
|
return {
|
|
|
|
|
labels: points.map((point) => point.label),
|
|
|
|
|
@ -605,14 +670,23 @@ function renderHistoricalChart(chartConfig, points) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function updateHistoricalTrends() {
|
|
|
|
|
const runtimeConfig = await readRuntimeConfig();
|
|
|
|
|
currentEnabledSensorIds = new Set(runtimeConfig.enabledSensors);
|
|
|
|
|
const chartData = await Promise.all(
|
|
|
|
|
historicalCharts.map(async (chartConfig) => ({
|
|
|
|
|
chartConfig,
|
|
|
|
|
points: await readHistoricalData(chartConfig)
|
|
|
|
|
points: currentEnabledSensorIds.has(chartConfig.sensorId)
|
|
|
|
|
? await readHistoricalData(chartConfig)
|
|
|
|
|
: []
|
|
|
|
|
}))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
chartData.forEach(({ chartConfig, points }) => {
|
|
|
|
|
if (!currentEnabledSensorIds.has(chartConfig.sensorId)) {
|
|
|
|
|
clearHistoricalChart(chartConfig, "Sensor deshabilitado");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderHistoricalChart(chartConfig, points);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
@ -669,11 +743,15 @@ window.updateLoggingRate = async function () {
|
|
|
|
|
|
|
|
|
|
async function loadLoggingRate() {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch('/api/config/logging', { cache: 'no-store' });
|
|
|
|
|
const response = await fetch('/api/config/runtime', { cache: 'no-store' });
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
|
|
|
|
|
if (!response.ok) return;
|
|
|
|
|
currentLoggingRateSeconds = result.rate;
|
|
|
|
|
if (Array.isArray(result.enabledSensors)) {
|
|
|
|
|
currentEnabledSensorIds = new Set(result.enabledSensors);
|
|
|
|
|
setEnabledSensorControls(result.enabledSensors);
|
|
|
|
|
}
|
|
|
|
|
document.getElementById("data-logging-rate").value = String(result.rate);
|
|
|
|
|
document.getElementById("sampling-interval").textContent =
|
|
|
|
|
`${result.rate} ${result.rate === 1 ? "segundo" : "segundos"}`;
|
|
|
|
|
@ -682,6 +760,51 @@ async function loadLoggingRate() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getSelectedEnabledSensors() {
|
|
|
|
|
return [...document.querySelectorAll('input[name="enabled-sensor"]:checked')]
|
|
|
|
|
.map((input) => input.value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setEnabledSensorControls(enabledSensors) {
|
|
|
|
|
const enabled = new Set(enabledSensors);
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll('input[name="enabled-sensor"]').forEach((input) => {
|
|
|
|
|
input.checked = enabled.has(input.value);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.updateEnabledSensors = async function () {
|
|
|
|
|
const enabledSensors = getSelectedEnabledSensors();
|
|
|
|
|
|
|
|
|
|
if (enabledSensors.length === 0) {
|
|
|
|
|
alert("Debe quedar al menos un sensor habilitado.");
|
|
|
|
|
setEnabledSensorControls([...currentEnabledSensorIds]);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch('/api/config/runtime', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
rate: currentLoggingRateSeconds,
|
|
|
|
|
enabledSensors
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
if (!response.ok) throw new Error(result.error || "No se pudo actualizar la configuracion");
|
|
|
|
|
|
|
|
|
|
currentEnabledSensorIds = new Set(result.enabledSensors);
|
|
|
|
|
setEnabledSensorControls(result.enabledSensors);
|
|
|
|
|
await updateDashboard();
|
|
|
|
|
alert(`Sensores habilitados: ${result.enabledSensors.join(", ")}`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error configurando sensores habilitados:", error);
|
|
|
|
|
alert("No se pudo actualizar la configuracion de sensores.");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
loadLoggingRate();
|
|
|
|
|
|
|
|
|
|
window.clearHistoricalData = async function () {
|
|
|
|
|
|