|
|
|
@ -1,61 +1,25 @@
|
|
|
|
const POLLING_INTERVAL_MS = 1000;
|
|
|
|
const POLLING_INTERVAL_MS = 1000;
|
|
|
|
|
|
|
|
const ALARM_CONFIG_FILE = "../config/alarms.json";
|
|
|
|
const WARNING_MARGIN_RATIO = 0.1;
|
|
|
|
const WARNING_MARGIN_RATIO = 0.1;
|
|
|
|
const MIN_READING_MAX_AGE_MS = 15000;
|
|
|
|
const MIN_READING_MAX_AGE_MS = 15000;
|
|
|
|
const DAY_IN_MS = 24 * 60 * 60 * 1000;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Variables dinámicas para el control de históricos
|
|
|
|
// Variables dinámicas para el control de históricos
|
|
|
|
let currentHistoryInterval = 10000;
|
|
|
|
let currentHistoryInterval = 10000;
|
|
|
|
let currentLoggingRateSeconds = 1;
|
|
|
|
let currentLoggingRateSeconds = 1;
|
|
|
|
let currentHistoryRetentionDays = 30;
|
|
|
|
|
|
|
|
let historyIntervalId = null;
|
|
|
|
let historyIntervalId = null;
|
|
|
|
let dashboardUpdateInProgress = false;
|
|
|
|
let dashboardUpdateInProgress = false;
|
|
|
|
let alarmControlsInitialized = false;
|
|
|
|
|
|
|
|
let notificationConfigLoaded = false;
|
|
|
|
|
|
|
|
let telegramTokenConfigured = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Diccionario para traducir estados en la UI sin romper las clases CSS
|
|
|
|
// Diccionario para traducir estados en la UI sin romper las clases CSS
|
|
|
|
const stateTranslations = {
|
|
|
|
const stateTranslations = {
|
|
|
|
"NORMAL": "NORMAL",
|
|
|
|
"NORMAL": "NORMAL",
|
|
|
|
"OFFLINE": "DESCONECTADO",
|
|
|
|
"OFFLINE": "DESCONECTADO",
|
|
|
|
"DISABLED": "DESHABILITADO",
|
|
|
|
|
|
|
|
"WARNING": "ADVERTENCIA",
|
|
|
|
"WARNING": "ADVERTENCIA",
|
|
|
|
"CRITICAL": "CRÍTICO"
|
|
|
|
"CRITICAL": "CRÍTICO"
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let currentEnabledSensorIds = new Set(["temperature", "ph", "do", "ec"]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getApiHeaders(extraHeaders = {}) {
|
|
|
|
|
|
|
|
const token = localStorage.getItem("photobioreactorApiToken") || "";
|
|
|
|
|
|
|
|
const headers = { ...extraHeaders };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (token) {
|
|
|
|
|
|
|
|
headers["X-API-Token"] = token;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return headers;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
window.getApiHeaders = getApiHeaders;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
window.saveApiToken = function () {
|
|
|
|
|
|
|
|
const input = document.getElementById("api-token-input");
|
|
|
|
|
|
|
|
const token = input.value.trim();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (token) {
|
|
|
|
|
|
|
|
localStorage.setItem("photobioreactorApiToken", token);
|
|
|
|
|
|
|
|
input.value = "";
|
|
|
|
|
|
|
|
alert("Token API guardado en este navegador.");
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
localStorage.removeItem("photobioreactorApiToken");
|
|
|
|
|
|
|
|
alert("Token API eliminado de este navegador.");
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const sensors = [
|
|
|
|
const sensors = [
|
|
|
|
{
|
|
|
|
{
|
|
|
|
id: "temperature",
|
|
|
|
id: "temperature",
|
|
|
|
sensorId: "temperature",
|
|
|
|
|
|
|
|
name: "Temperatura",
|
|
|
|
name: "Temperatura",
|
|
|
|
file: "../data/EZORTD.json",
|
|
|
|
file: "../data/EZORTD.json",
|
|
|
|
key: "temperature",
|
|
|
|
key: "temperature",
|
|
|
|
@ -99,7 +63,6 @@ const sensors = [
|
|
|
|
const historicalCharts = [
|
|
|
|
const historicalCharts = [
|
|
|
|
{
|
|
|
|
{
|
|
|
|
id: "temperature",
|
|
|
|
id: "temperature",
|
|
|
|
sensorId: "temperature",
|
|
|
|
|
|
|
|
title: "Temperatura vs Tiempo",
|
|
|
|
title: "Temperatura vs Tiempo",
|
|
|
|
file: "../logs/temperature.csv",
|
|
|
|
file: "../logs/temperature.csv",
|
|
|
|
valueKey: "temperature",
|
|
|
|
valueKey: "temperature",
|
|
|
|
@ -110,7 +73,6 @@ const historicalCharts = [
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
id: "ph-history",
|
|
|
|
id: "ph-history",
|
|
|
|
sensorId: "ph",
|
|
|
|
|
|
|
|
title: "pH vs Tiempo",
|
|
|
|
title: "pH vs Tiempo",
|
|
|
|
file: "../logs/ph.csv",
|
|
|
|
file: "../logs/ph.csv",
|
|
|
|
valueKey: "ph",
|
|
|
|
valueKey: "ph",
|
|
|
|
@ -121,7 +83,6 @@ const historicalCharts = [
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
id: "do-history",
|
|
|
|
id: "do-history",
|
|
|
|
sensorId: "do",
|
|
|
|
|
|
|
|
title: "Oxígeno Disuelto vs Tiempo",
|
|
|
|
title: "Oxígeno Disuelto vs Tiempo",
|
|
|
|
file: "../logs/do.csv",
|
|
|
|
file: "../logs/do.csv",
|
|
|
|
valueKey: "do",
|
|
|
|
valueKey: "do",
|
|
|
|
@ -132,7 +93,6 @@ const historicalCharts = [
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
id: "ec-history",
|
|
|
|
id: "ec-history",
|
|
|
|
sensorId: "ec",
|
|
|
|
|
|
|
|
title: "Conductividad vs Tiempo",
|
|
|
|
title: "Conductividad vs Tiempo",
|
|
|
|
file: "../logs/ec.csv",
|
|
|
|
file: "../logs/ec.csv",
|
|
|
|
valueKey: "ec",
|
|
|
|
valueKey: "ec",
|
|
|
|
@ -146,16 +106,6 @@ const historicalCharts = [
|
|
|
|
const chartInstances = new Map();
|
|
|
|
const chartInstances = new Map();
|
|
|
|
|
|
|
|
|
|
|
|
async function readSensor(sensor) {
|
|
|
|
async function readSensor(sensor) {
|
|
|
|
if (!currentEnabledSensorIds.has(sensor.id)) {
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
...sensor,
|
|
|
|
|
|
|
|
disabled: true,
|
|
|
|
|
|
|
|
online: false,
|
|
|
|
|
|
|
|
numericValue: null,
|
|
|
|
|
|
|
|
value: "--"
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const response = await fetch(`${sensor.file}?t=${Date.now()}`, {
|
|
|
|
const response = await fetch(`${sensor.file}?t=${Date.now()}`, {
|
|
|
|
cache: "no-store"
|
|
|
|
cache: "no-store"
|
|
|
|
@ -188,7 +138,6 @@ async function readSensor(sensor) {
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
...sensor,
|
|
|
|
...sensor,
|
|
|
|
disabled: false,
|
|
|
|
|
|
|
|
online: true,
|
|
|
|
online: true,
|
|
|
|
numericValue: rawValue,
|
|
|
|
numericValue: rawValue,
|
|
|
|
timestamp: data.timestamp || null,
|
|
|
|
timestamp: data.timestamp || null,
|
|
|
|
@ -199,7 +148,6 @@ async function readSensor(sensor) {
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
...sensor,
|
|
|
|
...sensor,
|
|
|
|
disabled: false,
|
|
|
|
|
|
|
|
online: false,
|
|
|
|
online: false,
|
|
|
|
numericValue: null,
|
|
|
|
numericValue: null,
|
|
|
|
value: "DESCONECTADO"
|
|
|
|
value: "DESCONECTADO"
|
|
|
|
@ -218,27 +166,25 @@ function setSensorState(result) {
|
|
|
|
// Traducir el estado para mostrar en pantalla, manteniendo la clase en inglés
|
|
|
|
// Traducir el estado para mostrar en pantalla, manteniendo la clase en inglés
|
|
|
|
stateElement.textContent = stateTranslations[state] || state;
|
|
|
|
stateElement.textContent = stateTranslations[state] || state;
|
|
|
|
|
|
|
|
|
|
|
|
["online", "normal", "warning", "critical", "offline", "disabled"].forEach((className) => {
|
|
|
|
["online", "normal", "warning", "critical", "offline"].forEach((className) => {
|
|
|
|
stateElement.classList.toggle(className, className === stateClass);
|
|
|
|
stateElement.classList.toggle(className, className === stateClass);
|
|
|
|
cardElement.classList.toggle(className, className === stateClass);
|
|
|
|
cardElement.classList.toggle(className, className === stateClass);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderSystemStatus(results) {
|
|
|
|
function renderSystemStatus(results) {
|
|
|
|
const enabledResults = results.filter((result) => !result.disabled);
|
|
|
|
const activeSensors = results.filter((result) => result.online).length;
|
|
|
|
const activeSensors = enabledResults.filter((result) => result.online).length;
|
|
|
|
const offlineSensors = results.length - activeSensors;
|
|
|
|
const offlineSensors = enabledResults.length - activeSensors;
|
|
|
|
|
|
|
|
const criticalSensors = results.filter((result) => result.alarmState === "CRITICAL").length;
|
|
|
|
const criticalSensors = results.filter((result) => result.alarmState === "CRITICAL").length;
|
|
|
|
const warningSensors = results.filter((result) => result.alarmState === "WARNING").length;
|
|
|
|
const warningSensors = results.filter((result) => result.alarmState === "WARNING").length;
|
|
|
|
const allNormal = enabledResults.length > 0 &&
|
|
|
|
const allNormal = results.every((result) => result.alarmState === "NORMAL");
|
|
|
|
enabledResults.every((result) => result.alarmState === "NORMAL");
|
|
|
|
|
|
|
|
const anyOnline = activeSensors > 0;
|
|
|
|
const anyOnline = activeSensors > 0;
|
|
|
|
const overallDot = document.getElementById("overall-dot");
|
|
|
|
const overallDot = document.getElementById("overall-dot");
|
|
|
|
const overallStatus = document.getElementById("overall-status");
|
|
|
|
const overallStatus = document.getElementById("overall-status");
|
|
|
|
const healthList = document.getElementById("sensor-health");
|
|
|
|
const healthList = document.getElementById("sensor-health");
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById("active-count").textContent =
|
|
|
|
document.getElementById("active-count").textContent =
|
|
|
|
`${activeSensors} / ${enabledResults.length}`;
|
|
|
|
`${activeSensors} / ${results.length}`;
|
|
|
|
document.getElementById("offline-count").textContent = String(offlineSensors);
|
|
|
|
document.getElementById("offline-count").textContent = String(offlineSensors);
|
|
|
|
document.getElementById("last-update").textContent =
|
|
|
|
document.getElementById("last-update").textContent =
|
|
|
|
new Date().toLocaleString();
|
|
|
|
new Date().toLocaleString();
|
|
|
|
@ -284,12 +230,10 @@ async function updateDashboard() {
|
|
|
|
dashboardUpdateInProgress = true;
|
|
|
|
dashboardUpdateInProgress = true;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const [runtimeConfig, alarmConfig] = await Promise.all([
|
|
|
|
const [results, alarmConfig] = await Promise.all([
|
|
|
|
readRuntimeConfig(),
|
|
|
|
Promise.all(sensors.map(readSensor)),
|
|
|
|
readAlarmConfig()
|
|
|
|
readAlarmConfig()
|
|
|
|
]);
|
|
|
|
]);
|
|
|
|
currentEnabledSensorIds = new Set(runtimeConfig.enabledSensors);
|
|
|
|
|
|
|
|
const results = await Promise.all(sensors.map(readSensor));
|
|
|
|
|
|
|
|
const evaluatedResults = results.map((result) =>
|
|
|
|
const evaluatedResults = results.map((result) =>
|
|
|
|
evaluateSensorAlarm(result, alarmConfig.thresholds)
|
|
|
|
evaluateSensorAlarm(result, alarmConfig.thresholds)
|
|
|
|
);
|
|
|
|
);
|
|
|
|
@ -297,100 +241,14 @@ async function updateDashboard() {
|
|
|
|
evaluatedResults.forEach(setSensorState);
|
|
|
|
evaluatedResults.forEach(setSensorState);
|
|
|
|
renderSystemStatus(evaluatedResults);
|
|
|
|
renderSystemStatus(evaluatedResults);
|
|
|
|
renderAlarmSummary(evaluatedResults, alarmConfig);
|
|
|
|
renderAlarmSummary(evaluatedResults, alarmConfig);
|
|
|
|
renderAlarmHistory();
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
} finally {
|
|
|
|
dashboardUpdateInProgress = false;
|
|
|
|
dashboardUpdateInProgress = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 loadNotificationConfig() {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const response = await fetch('/api/config/notifications', { cache: 'no-store' });
|
|
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok || !result.config) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const config = result.config;
|
|
|
|
|
|
|
|
telegramTokenConfigured = config.channels.telegram.botToken === "[configured]";
|
|
|
|
|
|
|
|
document.getElementById("notifications-enabled").checked = config.enabled === true;
|
|
|
|
|
|
|
|
document.getElementById("notification-min-severity").value = config.minSeverity || "CRITICAL";
|
|
|
|
|
|
|
|
document.getElementById("webhook-enabled").checked = config.channels.webhook.enabled === true;
|
|
|
|
|
|
|
|
document.getElementById("webhook-url").value = config.channels.webhook.url || "";
|
|
|
|
|
|
|
|
document.getElementById("telegram-enabled").checked = config.channels.telegram.enabled === true;
|
|
|
|
|
|
|
|
document.getElementById("telegram-chat-id").value = config.channels.telegram.chatId || "";
|
|
|
|
|
|
|
|
document.getElementById("telegram-bot-token").placeholder = telegramTokenConfigured
|
|
|
|
|
|
|
|
? "Token configurado"
|
|
|
|
|
|
|
|
: "Bot token";
|
|
|
|
|
|
|
|
notificationConfigLoaded = true;
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
|
|
console.warn("No se pudo cargar la configuracion de notificaciones:", error);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
window.saveNotificationConfig = async function () {
|
|
|
|
|
|
|
|
const botTokenInput = document.getElementById("telegram-bot-token");
|
|
|
|
|
|
|
|
const botToken = botTokenInput.value.trim();
|
|
|
|
|
|
|
|
const config = {
|
|
|
|
|
|
|
|
enabled: document.getElementById("notifications-enabled").checked,
|
|
|
|
|
|
|
|
minSeverity: document.getElementById("notification-min-severity").value,
|
|
|
|
|
|
|
|
channels: {
|
|
|
|
|
|
|
|
webhook: {
|
|
|
|
|
|
|
|
enabled: document.getElementById("webhook-enabled").checked,
|
|
|
|
|
|
|
|
url: document.getElementById("webhook-url").value.trim(),
|
|
|
|
|
|
|
|
headers: {}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
telegram: {
|
|
|
|
|
|
|
|
enabled: document.getElementById("telegram-enabled").checked,
|
|
|
|
|
|
|
|
botToken: botToken || (telegramTokenConfigured ? "__KEEP__" : ""),
|
|
|
|
|
|
|
|
chatId: document.getElementById("telegram-chat-id").value.trim()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const response = await fetch('/api/config/notifications', {
|
|
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
|
|
headers: getApiHeaders({ 'Content-Type': 'application/json' }),
|
|
|
|
|
|
|
|
body: JSON.stringify({ config })
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
|
|
throw new Error(result.error || "No se pudo guardar notificaciones.");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
botTokenInput.value = "";
|
|
|
|
|
|
|
|
notificationConfigLoaded = false;
|
|
|
|
|
|
|
|
await loadNotificationConfig();
|
|
|
|
|
|
|
|
alert("Configuracion de notificaciones actualizada.");
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
|
|
console.error("Error guardando notificaciones:", error);
|
|
|
|
|
|
|
|
alert(error.message);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function readAlarmConfig() {
|
|
|
|
async function readAlarmConfig() {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const response = await fetch('/api/config/alarms', {
|
|
|
|
const response = await fetch(`${ALARM_CONFIG_FILE}?t=${Date.now()}`, {
|
|
|
|
cache: "no-store"
|
|
|
|
cache: "no-store"
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
@ -398,10 +256,7 @@ async function readAlarmConfig() {
|
|
|
|
throw new Error(`HTTP ${response.status}`);
|
|
|
|
throw new Error(`HTTP ${response.status}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
const thresholds = await response.json();
|
|
|
|
const thresholds = result.thresholds || result;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
renderAlarmThresholdControls(thresholds);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
loaded: true,
|
|
|
|
loaded: true,
|
|
|
|
@ -417,86 +272,7 @@ async function readAlarmConfig() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderAlarmThresholdControls(thresholds) {
|
|
|
|
|
|
|
|
const container = document.getElementById("alarm-threshold-controls");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!container || alarmControlsInitialized) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
container.innerHTML = sensors
|
|
|
|
|
|
|
|
.map((sensor) => {
|
|
|
|
|
|
|
|
const limits = thresholds[sensor.id] || {};
|
|
|
|
|
|
|
|
const min = Number.isFinite(Number(limits.min)) ? Number(limits.min) : "";
|
|
|
|
|
|
|
|
const max = Number.isFinite(Number(limits.max)) ? Number(limits.max) : "";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
|
|
|
<div class="threshold-group">
|
|
|
|
|
|
|
|
<strong>${sensor.name}</strong>
|
|
|
|
|
|
|
|
<label>
|
|
|
|
|
|
|
|
Min
|
|
|
|
|
|
|
|
<input type="number" step="0.001" data-threshold-sensor="${sensor.id}" data-threshold-bound="min" value="${min}">
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
<label>
|
|
|
|
|
|
|
|
Max
|
|
|
|
|
|
|
|
<input type="number" step="0.001" data-threshold-sensor="${sensor.id}" data-threshold-bound="max" value="${max}">
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
`})
|
|
|
|
|
|
|
|
.join("");
|
|
|
|
|
|
|
|
alarmControlsInitialized = true;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function collectAlarmThresholdControls() {
|
|
|
|
|
|
|
|
const thresholds = {};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sensors.forEach((sensor) => {
|
|
|
|
|
|
|
|
const minInput = document.querySelector(`[data-threshold-sensor="${sensor.id}"][data-threshold-bound="min"]`);
|
|
|
|
|
|
|
|
const maxInput = document.querySelector(`[data-threshold-sensor="${sensor.id}"][data-threshold-bound="max"]`);
|
|
|
|
|
|
|
|
const min = Number(minInput && minInput.value);
|
|
|
|
|
|
|
|
const max = Number(maxInput && maxInput.value);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!Number.isFinite(min) || !Number.isFinite(max) || min >= max) {
|
|
|
|
|
|
|
|
throw new Error(`Umbrales invalidos para ${sensor.name}.`);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
thresholds[sensor.id] = { min, max };
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return thresholds;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
window.saveAlarmThresholds = async function () {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const thresholds = collectAlarmThresholdControls();
|
|
|
|
|
|
|
|
const response = await fetch('/api/config/alarms', {
|
|
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
|
|
headers: getApiHeaders({ 'Content-Type': 'application/json' }),
|
|
|
|
|
|
|
|
body: JSON.stringify({ thresholds })
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
|
|
throw new Error(result.error || "No se pudieron guardar los umbrales.");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alarmControlsInitialized = false;
|
|
|
|
|
|
|
|
renderAlarmThresholdControls(result.thresholds);
|
|
|
|
|
|
|
|
await updateDashboard();
|
|
|
|
|
|
|
|
alert("Umbrales de alarma actualizados.");
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
|
|
console.error("Error guardando umbrales:", error);
|
|
|
|
|
|
|
|
alert(error.message);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function evaluateSensorAlarm(sensorResult, thresholds) {
|
|
|
|
function evaluateSensorAlarm(sensorResult, thresholds) {
|
|
|
|
if (sensorResult.disabled) {
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
...sensorResult,
|
|
|
|
|
|
|
|
alarmState: "DISABLED",
|
|
|
|
|
|
|
|
alarmMessage: "Sensor no habilitado en la configuracion actual"
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!sensorResult.online) {
|
|
|
|
if (!sensorResult.online) {
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
...sensorResult,
|
|
|
|
...sensorResult,
|
|
|
|
@ -566,7 +342,7 @@ function buildAlarmMessage(sensorResult, limits, alarmType) {
|
|
|
|
|
|
|
|
|
|
|
|
function getAlarmEvents(results) {
|
|
|
|
function getAlarmEvents(results) {
|
|
|
|
return results
|
|
|
|
return results
|
|
|
|
.filter((result) => !["NORMAL", "DISABLED"].includes(result.alarmState))
|
|
|
|
.filter((result) => result.alarmState !== "NORMAL")
|
|
|
|
.map((result) => ({
|
|
|
|
.map((result) => ({
|
|
|
|
sensorId: result.id,
|
|
|
|
sensorId: result.id,
|
|
|
|
sensorName: result.name,
|
|
|
|
sensorName: result.name,
|
|
|
|
@ -631,50 +407,6 @@ function renderAlarmSummary(results, alarmConfig) {
|
|
|
|
.join("");
|
|
|
|
.join("");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function readAlarmHistory() {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const response = await fetch('/api/alarms', { cache: 'no-store' });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
|
|
throw new Error(`HTTP ${response.status}`);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return await response.json();
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
|
|
console.warn("No se pudo cargar la bitacora de alarmas:", error);
|
|
|
|
|
|
|
|
return [];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function renderAlarmHistory() {
|
|
|
|
|
|
|
|
const historyList = document.getElementById("alarm-history-list");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!historyList) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const events = await readAlarmHistory();
|
|
|
|
|
|
|
|
const recentEvents = events.slice(-8).reverse();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (recentEvents.length === 0) {
|
|
|
|
|
|
|
|
historyList.innerHTML = '<li class="alarm-empty">Sin eventos registrados</li>';
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
historyList.innerHTML = recentEvents
|
|
|
|
|
|
|
|
.map((event) => {
|
|
|
|
|
|
|
|
const stateClass = String(event.state || "").toLowerCase();
|
|
|
|
|
|
|
|
const timestamp = event.timestamp
|
|
|
|
|
|
|
|
? new Date(event.timestamp).toLocaleString()
|
|
|
|
|
|
|
|
: "--";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
|
|
|
<li class="${stateClass}">
|
|
|
|
|
|
|
|
<span>${timestamp} · ${event.sensor}</span>
|
|
|
|
|
|
|
|
<span>${event.state}: ${event.message}</span>
|
|
|
|
|
|
|
|
</li>
|
|
|
|
|
|
|
|
`})
|
|
|
|
|
|
|
|
.join("");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function readHistoricalData(chartConfig) {
|
|
|
|
async function readHistoricalData(chartConfig) {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const response = await fetch(`${chartConfig.file}?t=${Date.now()}`, {
|
|
|
|
const response = await fetch(`${chartConfig.file}?t=${Date.now()}`, {
|
|
|
|
@ -719,7 +451,6 @@ function parseHistoricalCsv(csvText, valueKey) {
|
|
|
|
return rows
|
|
|
|
return rows
|
|
|
|
.map((line) => splitCsvLine(line))
|
|
|
|
.map((line) => splitCsvLine(line))
|
|
|
|
.map((columns) => {
|
|
|
|
.map((columns) => {
|
|
|
|
const rawTimestamp = columns[timestampIndex];
|
|
|
|
|
|
|
|
const rawValue = Number(columns[valueIndex]);
|
|
|
|
const rawValue = Number(columns[valueIndex]);
|
|
|
|
|
|
|
|
|
|
|
|
if (!Number.isFinite(rawValue)) {
|
|
|
|
if (!Number.isFinite(rawValue)) {
|
|
|
|
@ -727,49 +458,13 @@ function parseHistoricalCsv(csvText, valueKey) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
timestampMs: parseTimestampMs(rawTimestamp),
|
|
|
|
label: formatTimestamp(columns[timestampIndex]),
|
|
|
|
label: formatTimestamp(rawTimestamp),
|
|
|
|
|
|
|
|
value: rawValue
|
|
|
|
value: rawValue
|
|
|
|
};
|
|
|
|
};
|
|
|
|
})
|
|
|
|
})
|
|
|
|
.filter(Boolean);
|
|
|
|
.filter(Boolean);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parseTimestampMs(rawTimestamp) {
|
|
|
|
|
|
|
|
const numericTimestamp = Number(rawTimestamp);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (Number.isFinite(numericTimestamp)) {
|
|
|
|
|
|
|
|
if (numericTimestamp > 1000000000000) {
|
|
|
|
|
|
|
|
return numericTimestamp;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (numericTimestamp > 1000000000) {
|
|
|
|
|
|
|
|
return numericTimestamp * 1000;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const parsedDate = new Date(rawTimestamp);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!Number.isNaN(parsedDate.getTime())) {
|
|
|
|
|
|
|
|
return parsedDate.getTime();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return Number.NaN;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function filterPointsForCurrentDay(points) {
|
|
|
|
|
|
|
|
const startOfToday = new Date();
|
|
|
|
|
|
|
|
startOfToday.setHours(0, 0, 0, 0);
|
|
|
|
|
|
|
|
const startMs = startOfToday.getTime();
|
|
|
|
|
|
|
|
const endMs = startMs + DAY_IN_MS;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return points.filter((point) =>
|
|
|
|
|
|
|
|
Number.isFinite(point.timestampMs) &&
|
|
|
|
|
|
|
|
point.timestampMs >= startMs &&
|
|
|
|
|
|
|
|
point.timestampMs < endMs
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function splitCsvLine(line) {
|
|
|
|
function splitCsvLine(line) {
|
|
|
|
return line
|
|
|
|
return line
|
|
|
|
.split(",")
|
|
|
|
.split(",")
|
|
|
|
@ -793,35 +488,32 @@ function formatTimestamp(rawTimestamp) {
|
|
|
|
return "";
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const parsedMs = parseTimestampMs(rawTimestamp);
|
|
|
|
const numericTimestamp = Number(rawTimestamp);
|
|
|
|
|
|
|
|
|
|
|
|
if (Number.isFinite(parsedMs)) {
|
|
|
|
if (Number.isFinite(numericTimestamp)) {
|
|
|
|
return new Date(parsedMs).toLocaleTimeString();
|
|
|
|
if (numericTimestamp > 1000000000000) {
|
|
|
|
|
|
|
|
return new Date(numericTimestamp).toLocaleTimeString();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return rawTimestamp;
|
|
|
|
if (numericTimestamp > 1000000000) {
|
|
|
|
|
|
|
|
return new Date(numericTimestamp * 1000).toLocaleTimeString();
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setChartAvailability(chartConfig, hasData, message = "No hay datos históricos disponibles") {
|
|
|
|
const parsedDate = new Date(rawTimestamp);
|
|
|
|
const emptyElement = document.getElementById(chartConfig.emptyElement);
|
|
|
|
|
|
|
|
const frameElement = emptyElement.closest(".chart-frame");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
emptyElement.textContent = message;
|
|
|
|
if (!Number.isNaN(parsedDate.getTime())) {
|
|
|
|
frameElement.classList.toggle("empty", !hasData);
|
|
|
|
return parsedDate.toLocaleTimeString();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clearHistoricalChart(chartConfig, message) {
|
|
|
|
return rawTimestamp;
|
|
|
|
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 setChartAvailability(chartConfig, hasData) {
|
|
|
|
|
|
|
|
const emptyElement = document.getElementById(chartConfig.emptyElement);
|
|
|
|
|
|
|
|
const frameElement = emptyElement.closest(".chart-frame");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frameElement.classList.toggle("empty", !hasData);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildChartDataset(chartConfig, points) {
|
|
|
|
function buildChartDataset(chartConfig, points) {
|
|
|
|
@ -913,23 +605,14 @@ function renderHistoricalChart(chartConfig, points) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function updateHistoricalTrends() {
|
|
|
|
async function updateHistoricalTrends() {
|
|
|
|
const runtimeConfig = await readRuntimeConfig();
|
|
|
|
|
|
|
|
currentEnabledSensorIds = new Set(runtimeConfig.enabledSensors);
|
|
|
|
|
|
|
|
const chartData = await Promise.all(
|
|
|
|
const chartData = await Promise.all(
|
|
|
|
historicalCharts.map(async (chartConfig) => ({
|
|
|
|
historicalCharts.map(async (chartConfig) => ({
|
|
|
|
chartConfig,
|
|
|
|
chartConfig,
|
|
|
|
points: currentEnabledSensorIds.has(chartConfig.sensorId)
|
|
|
|
points: await readHistoricalData(chartConfig)
|
|
|
|
? filterPointsForCurrentDay(await readHistoricalData(chartConfig))
|
|
|
|
|
|
|
|
: []
|
|
|
|
|
|
|
|
}))
|
|
|
|
}))
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
chartData.forEach(({ chartConfig, points }) => {
|
|
|
|
chartData.forEach(({ chartConfig, points }) => {
|
|
|
|
if (!currentEnabledSensorIds.has(chartConfig.sensorId)) {
|
|
|
|
|
|
|
|
clearHistoricalChart(chartConfig, "Sensor deshabilitado");
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
renderHistoricalChart(chartConfig, points);
|
|
|
|
renderHistoricalChart(chartConfig, points);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -948,7 +631,6 @@ updateDashboard();
|
|
|
|
setInterval(updateDashboard, POLLING_INTERVAL_MS);
|
|
|
|
setInterval(updateDashboard, POLLING_INTERVAL_MS);
|
|
|
|
updateHistoricalTrends();
|
|
|
|
updateHistoricalTrends();
|
|
|
|
startHistoryPolling();
|
|
|
|
startHistoryPolling();
|
|
|
|
loadNotificationConfig();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// --- FUNCIONES DE CONTROL DE ALMACENAMIENTO ---
|
|
|
|
// --- FUNCIONES DE CONTROL DE ALMACENAMIENTO ---
|
|
|
|
|
|
|
|
|
|
|
|
@ -969,7 +651,7 @@ window.updateLoggingRate = async function () {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const response = await fetch('/api/config/logging', {
|
|
|
|
const response = await fetch('/api/config/logging', {
|
|
|
|
method: 'POST',
|
|
|
|
method: 'POST',
|
|
|
|
headers: getApiHeaders({ 'Content-Type': 'application/json' }),
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
body: JSON.stringify({ rate })
|
|
|
|
body: JSON.stringify({ rate })
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
@ -987,18 +669,11 @@ window.updateLoggingRate = async function () {
|
|
|
|
|
|
|
|
|
|
|
|
async function loadLoggingRate() {
|
|
|
|
async function loadLoggingRate() {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const response = await fetch('/api/config/runtime', { cache: 'no-store' });
|
|
|
|
const response = await fetch('/api/config/logging', { cache: 'no-store' });
|
|
|
|
const result = await response.json();
|
|
|
|
const result = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) return;
|
|
|
|
if (!response.ok) return;
|
|
|
|
currentLoggingRateSeconds = result.rate;
|
|
|
|
currentLoggingRateSeconds = result.rate;
|
|
|
|
if (Array.isArray(result.enabledSensors)) {
|
|
|
|
|
|
|
|
currentEnabledSensorIds = new Set(result.enabledSensors);
|
|
|
|
|
|
|
|
setEnabledSensorControls(result.enabledSensors);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
currentHistoryRetentionDays = Number(result.historyRetentionDays ?? 30);
|
|
|
|
|
|
|
|
document.getElementById("history-retention-days").value =
|
|
|
|
|
|
|
|
String(currentHistoryRetentionDays);
|
|
|
|
|
|
|
|
document.getElementById("data-logging-rate").value = String(result.rate);
|
|
|
|
document.getElementById("data-logging-rate").value = String(result.rate);
|
|
|
|
document.getElementById("sampling-interval").textContent =
|
|
|
|
document.getElementById("sampling-interval").textContent =
|
|
|
|
`${result.rate} ${result.rate === 1 ? "segundo" : "segundos"}`;
|
|
|
|
`${result.rate} ${result.rate === 1 ? "segundo" : "segundos"}`;
|
|
|
|
@ -1007,84 +682,6 @@ async function loadLoggingRate() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
window.updateHistoryRetention = async function () {
|
|
|
|
|
|
|
|
const select = document.getElementById("history-retention-days");
|
|
|
|
|
|
|
|
const historyRetentionDays = Number(select.value);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const response = await fetch('/api/config/runtime', {
|
|
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
|
|
headers: getApiHeaders({ 'Content-Type': 'application/json' }),
|
|
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
|
|
rate: currentLoggingRateSeconds,
|
|
|
|
|
|
|
|
historyRetentionDays,
|
|
|
|
|
|
|
|
enabledSensors: [...currentEnabledSensorIds]
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) throw new Error(result.error || "No se pudo actualizar la retencion");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
currentHistoryRetentionDays = result.historyRetentionDays;
|
|
|
|
|
|
|
|
select.value = String(result.historyRetentionDays);
|
|
|
|
|
|
|
|
alert(
|
|
|
|
|
|
|
|
result.historyRetentionDays === 0
|
|
|
|
|
|
|
|
? "Retención histórica desactivada."
|
|
|
|
|
|
|
|
: `Retención histórica actualizada a ${result.historyRetentionDays} días.`
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
|
|
console.error("Error configurando retencion historica:", error);
|
|
|
|
|
|
|
|
select.value = String(currentHistoryRetentionDays);
|
|
|
|
|
|
|
|
alert("No se pudo actualizar la retención histórica.");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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: getApiHeaders({ 'Content-Type': 'application/json' }),
|
|
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
|
|
rate: currentLoggingRateSeconds,
|
|
|
|
|
|
|
|
historyRetentionDays: currentHistoryRetentionDays,
|
|
|
|
|
|
|
|
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();
|
|
|
|
loadLoggingRate();
|
|
|
|
|
|
|
|
|
|
|
|
window.clearHistoricalData = async function () {
|
|
|
|
window.clearHistoricalData = async function () {
|
|
|
|
@ -1093,10 +690,7 @@ window.clearHistoricalData = async function () {
|
|
|
|
if (!confirmacion) return;
|
|
|
|
if (!confirmacion) return;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const response = await fetch('/api/history/clear', {
|
|
|
|
const response = await fetch('/api/history/clear', { method: 'POST' });
|
|
|
|
method: 'POST',
|
|
|
|
|
|
|
|
headers: getApiHeaders()
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!response.ok) throw new Error("Fallo en el purgado de archivos");
|
|
|
|
if (!response.ok) throw new Error("Fallo en el purgado de archivos");
|
|
|
|
|
|
|
|
|
|
|
|
// Limpia los datasets existentes sin recrear las gráficas.
|
|
|
|
// Limpia los datasets existentes sin recrear las gráficas.
|
|
|
|
|