const POLLING_INTERVAL_MS = 1000; const WARNING_MARGIN_RATIO = 0.1; const MIN_READING_MAX_AGE_MS = 15000; // Variables dinámicas para el control de históricos let currentHistoryInterval = 10000; let currentLoggingRateSeconds = 1; let currentHistoryRetentionDays = 30; let historyIntervalId = null; let dashboardUpdateInProgress = false; let alarmControlsInitialized = false; // Diccionario para traducir estados en la UI sin romper las clases CSS const stateTranslations = { "NORMAL": "NORMAL", "OFFLINE": "DESCONECTADO", "DISABLED": "DESHABILITADO", "WARNING": "ADVERTENCIA", "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 = [ { id: "temperature", sensorId: "temperature", name: "Temperatura", file: "../data/EZORTD.json", key: "temperature", decimals: 3, valueElement: "temperature-value", stateElement: "temperature-state", cardElement: "card-temperature" }, { id: "ph", name: "pH", file: "../data/EZOPH.json", key: "ph", decimals: 2, valueElement: "ph-value", stateElement: "ph-state", cardElement: "card-ph" }, { id: "do", name: "Oxígeno Disuelto", file: "../data/EZODO.json", key: "do", decimals: 2, valueElement: "do-value", stateElement: "do-state", cardElement: "card-do" }, { id: "ec", name: "Conductividad", file: "../data/EZOEC.json", key: "ec", decimals: 0, valueElement: "ec-value", stateElement: "ec-state", cardElement: "card-ec" } ]; const historicalCharts = [ { id: "temperature", sensorId: "temperature", title: "Temperatura vs Tiempo", file: "../logs/temperature.csv", valueKey: "temperature", unit: "degC", canvasElement: "temperature-chart", emptyElement: "temperature-chart-empty", color: "#097969" }, { id: "ph-history", sensorId: "ph", title: "pH vs Tiempo", file: "../logs/ph.csv", valueKey: "ph", unit: "pH", canvasElement: "ph-chart", emptyElement: "ph-chart-empty", color: "#4f46e5" }, { id: "do-history", sensorId: "do", title: "Oxígeno Disuelto vs Tiempo", file: "../logs/do.csv", valueKey: "do", unit: "mg/L", canvasElement: "do-chart", emptyElement: "do-chart-empty", color: "#0f766e" }, { id: "ec-history", sensorId: "ec", title: "Conductividad vs Tiempo", file: "../logs/ec.csv", valueKey: "ec", unit: "uS/cm", canvasElement: "ec-chart", emptyElement: "ec-chart-empty", color: "#b97300" } ]; 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" }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); const rawValue = Number(data[sensor.key]); const readingTimestamp = data.timestamp ? new Date(data.timestamp).getTime() : null; if (data.online === false || !Number.isFinite(rawValue)) { throw new Error(`Invalid value for ${sensor.key}`); } if ( Number.isFinite(readingTimestamp) && Date.now() - readingTimestamp > Math.max( MIN_READING_MAX_AGE_MS, currentLoggingRateSeconds * 2500 ) ) { throw new Error(`Stale value for ${sensor.key}`); } return { ...sensor, disabled: false, online: true, numericValue: rawValue, timestamp: data.timestamp || null, value: rawValue.toFixed(sensor.decimals) }; } catch (error) { console.warn(`${sensor.name} offline:`, error); return { ...sensor, disabled: false, online: false, numericValue: null, value: "DESCONECTADO" }; } } function setSensorState(result) { const valueElement = document.getElementById(result.valueElement); const stateElement = document.getElementById(result.stateElement); const cardElement = document.getElementById(result.cardElement); const state = result.alarmState || (result.online ? "NORMAL" : "OFFLINE"); const stateClass = state.toLowerCase(); valueElement.textContent = result.value; // Traducir el estado para mostrar en pantalla, manteniendo la clase en inglés stateElement.textContent = stateTranslations[state] || state; ["online", "normal", "warning", "critical", "offline", "disabled"].forEach((className) => { stateElement.classList.toggle(className, className === stateClass); cardElement.classList.toggle(className, className === stateClass); }); } function renderSystemStatus(results) { 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 = 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} / ${enabledResults.length}`; document.getElementById("offline-count").textContent = String(offlineSensors); document.getElementById("last-update").textContent = new Date().toLocaleString(); overallDot.classList.toggle("online", allNormal); overallDot.classList.toggle("warning", warningSensors > 0 && criticalSensors === 0); overallDot.classList.toggle("critical", criticalSensors > 0); overallDot.classList.toggle("offline", !anyOnline); if (criticalSensors > 0) { overallStatus.textContent = "ALARMA CRÍTICA"; } else if (warningSensors > 0) { overallStatus.textContent = "ADVERTENCIA ACTIVA"; } else if (allNormal) { overallStatus.textContent = "TODOS LOS SISTEMAS NORMALES"; } else if (anyOnline) { overallStatus.textContent = "DATOS PARCIALES"; } else { overallStatus.textContent = "SISTEMA DESCONECTADO"; } healthList.innerHTML = results .map((result) => { const statusText = result.alarmState || (result.online ? "NORMAL" : "OFFLINE"); const statusClass = statusText.toLowerCase(); const translatedStatus = stateTranslations[statusText] || statusText; return `
  • ${result.name} ${translatedStatus}
  • `; }) .join(""); } async function updateDashboard() { if (dashboardUpdateInProgress) { return; } dashboardUpdateInProgress = true; try { 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) ); evaluatedResults.forEach(setSensorState); renderSystemStatus(evaluatedResults); renderAlarmSummary(evaluatedResults, alarmConfig); renderAlarmHistory(); } finally { 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 readAlarmConfig() { try { const response = await fetch('/api/config/alarms', { cache: "no-store" }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const result = await response.json(); const thresholds = result.thresholds || result; renderAlarmThresholdControls(thresholds); return { loaded: true, thresholds }; } catch (error) { console.warn("Alarm configuration unavailable:", error); return { loaded: false, thresholds: {} }; } } 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 `
    ${sensor.name}
    `}) .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) { if (sensorResult.disabled) { return { ...sensorResult, alarmState: "DISABLED", alarmMessage: "Sensor no habilitado en la configuracion actual" }; } if (!sensorResult.online) { return { ...sensorResult, alarmState: "OFFLINE", alarmMessage: "Datos del sensor no disponibles" }; } const limits = thresholds[sensorResult.id]; if (!isValidThreshold(limits)) { return { ...sensorResult, alarmState: "WARNING", alarmMessage: "Límites de alarma no disponibles" }; } if (sensorResult.numericValue < limits.min || sensorResult.numericValue > limits.max) { return { ...sensorResult, alarmState: "CRITICAL", alarmMessage: buildAlarmMessage(sensorResult, limits, "outside") }; } if (isNearThreshold(sensorResult.numericValue, limits)) { return { ...sensorResult, alarmState: "WARNING", alarmMessage: buildAlarmMessage(sensorResult, limits, "near") }; } return { ...sensorResult, alarmState: "NORMAL", alarmMessage: "Dentro del rango configurado" }; } function isValidThreshold(limits) { return Boolean(limits) && Number.isFinite(Number(limits.min)) && Number.isFinite(Number(limits.max)) && Number(limits.min) < Number(limits.max); } function isNearThreshold(value, limits) { const min = Number(limits.min); const max = Number(limits.max); const warningMargin = (max - min) * WARNING_MARGIN_RATIO; return value <= min + warningMargin || value >= max - warningMargin; } function buildAlarmMessage(sensorResult, limits, alarmType) { const direction = sensorResult.numericValue < limits.min ? "por debajo del" : "por encima del"; const range = `${limits.min} - ${limits.max}`; if (alarmType === "outside") { return `${sensorResult.value} está ${direction} rango configurado (${range})`; } return `${sensorResult.value} está cerca del rango configurado (${range})`; } function getAlarmEvents(results) { return results .filter((result) => !["NORMAL", "DISABLED"].includes(result.alarmState)) .map((result) => ({ sensorId: result.id, sensorName: result.name, severity: result.alarmState, value: result.value, message: result.alarmMessage, createdAt: new Date().toISOString() })); } function getOverallRiskLevel(results) { if (results.some((result) => result.alarmState === "CRITICAL")) { return "CRITICAL"; } if (results.some((result) => result.alarmState === "WARNING")) { return "WARNING"; } if (results.some((result) => result.alarmState === "OFFLINE")) { return "OFFLINE"; } return "NORMAL"; } function renderAlarmSummary(results, alarmConfig) { const criticalCount = results.filter((result) => result.alarmState === "CRITICAL").length; const warningCount = results.filter((result) => result.alarmState === "WARNING").length; const offlineCount = results.filter((result) => result.alarmState === "OFFLINE").length; const overallRisk = getOverallRiskLevel(results); const alarmEvents = getAlarmEvents(results); const riskElement = document.getElementById("overall-risk"); const alarmList = document.getElementById("alarm-list"); document.getElementById("critical-count").textContent = String(criticalCount); document.getElementById("warning-count").textContent = String(warningCount); document.getElementById("alarm-offline-count").textContent = String(offlineCount); document.getElementById("alarm-config-status").textContent = alarmConfig.loaded ? "cargados" : "no disponibles"; riskElement.textContent = stateTranslations[overallRisk] || overallRisk; ["risk-normal", "risk-warning", "risk-critical", "risk-offline"].forEach((className) => { riskElement.classList.remove(className); }); riskElement.classList.add(`risk-${overallRisk.toLowerCase()}`); if (alarmEvents.length === 0) { alarmList.innerHTML = '
  • Sin alarmas activas
  • '; return; } alarmList.innerHTML = alarmEvents .map((event) => { const translatedSeverity = stateTranslations[event.severity] || event.severity; return `
  • ${event.sensorName} ${translatedSeverity}: ${event.message}
  • `}) .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 = '
  • Sin eventos registrados
  • '; return; } historyList.innerHTML = recentEvents .map((event) => { const stateClass = String(event.state || "").toLowerCase(); const timestamp = event.timestamp ? new Date(event.timestamp).toLocaleString() : "--"; return `
  • ${timestamp} · ${event.sensor} ${event.state}: ${event.message}
  • `}) .join(""); } async function readHistoricalData(chartConfig) { try { const response = await fetch(`${chartConfig.file}?t=${Date.now()}`, { cache: "no-store" }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const csvText = await response.text(); return parseHistoricalCsv(csvText, chartConfig.valueKey); } catch (error) { console.warn(`${chartConfig.title} offline:`, error); return []; } } function parseHistoricalCsv(csvText, valueKey) { const lines = csvText .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean); if (lines.length === 0) { return []; } const firstRow = splitCsvLine(lines[0]); const lowerFirstRow = firstRow.map((cell) => cell.toLowerCase()); const hasNamedHeader = lowerFirstRow.some((cell) => ["timestamp", "time", "date", valueKey, "value", "reading"].some((candidate) => cell.includes(candidate) ) ); const hasHeader = hasNamedHeader || !Number.isFinite(Number(firstRow[1])); const header = hasHeader ? firstRow.map((cell) => cell.toLowerCase()) : []; const rows = hasHeader ? lines.slice(1) : lines; const timestampIndex = getCsvColumnIndex(header, ["timestamp", "time", "date"], 0); const valueIndex = getCsvColumnIndex(header, [valueKey, "value", "reading"], 1); return rows .map((line) => splitCsvLine(line)) .map((columns) => { const rawValue = Number(columns[valueIndex]); if (!Number.isFinite(rawValue)) { return null; } return { label: formatTimestamp(columns[timestampIndex]), value: rawValue }; }) .filter(Boolean); } function splitCsvLine(line) { return line .split(",") .map((cell) => cell.trim().replace(/^"|"$/g, "")); } function getCsvColumnIndex(header, candidates, fallbackIndex) { if (header.length === 0) { return fallbackIndex; } const index = header.findIndex((columnName) => candidates.some((candidate) => columnName.includes(candidate)) ); return index >= 0 ? index : fallbackIndex; } function formatTimestamp(rawTimestamp) { if (!rawTimestamp) { return ""; } const numericTimestamp = Number(rawTimestamp); if (Number.isFinite(numericTimestamp)) { if (numericTimestamp > 1000000000000) { return new Date(numericTimestamp).toLocaleTimeString(); } if (numericTimestamp > 1000000000) { return new Date(numericTimestamp * 1000).toLocaleTimeString(); } } const parsedDate = new Date(rawTimestamp); if (!Number.isNaN(parsedDate.getTime())) { return parsedDate.toLocaleTimeString(); } return rawTimestamp; } 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), datasets: [ { label: `${chartConfig.title} (${chartConfig.unit})`, data: points.map((point) => point.value), borderColor: chartConfig.color, backgroundColor: `${chartConfig.color}1f`, borderWidth: 2, pointRadius: 2, pointHoverRadius: 4, tension: 0.32, fill: true } ] }; } function getChartOptions(chartConfig) { return { responsive: true, maintainAspectRatio: false, animation: false, plugins: { legend: { display: false }, tooltip: { callbacks: { label(context) { return `${context.parsed.y} ${chartConfig.unit}`; } } } }, scales: { x: { ticks: { maxRotation: 0, autoSkip: true, maxTicksLimit: 6 }, grid: { display: false } }, y: { beginAtZero: false, ticks: { callback(value) { return `${value}`; } } } } }; } function renderHistoricalChart(chartConfig, points) { const chartLibraryReady = typeof Chart !== "undefined"; const hasData = points.length > 0 && chartLibraryReady; const existingChart = chartInstances.get(chartConfig.id); setChartAvailability(chartConfig, hasData); if (!hasData) { return; } const chartData = buildChartDataset(chartConfig, points); if (existingChart) { existingChart.data = chartData; existingChart.update("none"); return; } const canvas = document.getElementById(chartConfig.canvasElement); const chart = new Chart(canvas, { type: "line", data: chartData, options: getChartOptions(chartConfig) }); chartInstances.set(chartConfig.id, chart); } async function updateHistoricalTrends() { const runtimeConfig = await readRuntimeConfig(); currentEnabledSensorIds = new Set(runtimeConfig.enabledSensors); const chartData = await Promise.all( historicalCharts.map(async (chartConfig) => ({ 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); }); } // --- MOTOR DE INTERVALOS DINÁMICOS --- function startHistoryPolling() { if (historyIntervalId) { clearInterval(historyIntervalId); } historyIntervalId = setInterval(updateHistoricalTrends, currentHistoryInterval); } // Inicialización de renderizado updateDashboard(); setInterval(updateDashboard, POLLING_INTERVAL_MS); updateHistoricalTrends(); startHistoryPolling(); // --- FUNCIONES DE CONTROL DE ALMACENAMIENTO --- window.updateChartRefreshRate = function () { const select = document.getElementById("chart-refresh-rate"); currentHistoryInterval = parseInt(select.value, 10); const intervalText = select.options[select.selectedIndex].text.replace("Cada ", "").toLowerCase(); document.getElementById("history-interval").textContent = intervalText; startHistoryPolling(); }; window.updateLoggingRate = async function () { const select = document.getElementById("data-logging-rate"); const rate = parseInt(select.value, 10); try { const response = await fetch('/api/config/logging', { method: 'POST', headers: getApiHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify({ rate }) }); if (!response.ok) throw new Error("Fallo en la sincronización con el servidor"); const result = await response.json(); currentLoggingRateSeconds = result.rate; document.getElementById("sampling-interval").textContent = `${result.rate} ${result.rate === 1 ? "segundo" : "segundos"}`; alert(`Frecuencia de adquisición actualizada a ${result.rate} segundos.`); } catch (error) { console.error("Error configurando la tasa de registro:", error); } }; async function loadLoggingRate() { try { 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); } currentHistoryRetentionDays = Number(result.historyRetentionDays ?? 30); document.getElementById("history-retention-days").value = String(currentHistoryRetentionDays); document.getElementById("data-logging-rate").value = String(result.rate); document.getElementById("sampling-interval").textContent = `${result.rate} ${result.rate === 1 ? "segundo" : "segundos"}`; } catch (error) { console.warn("No se pudo cargar la frecuencia de adquisición:", error); } } 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(); window.clearHistoricalData = async function () { const confirmacion = confirm("ADVERTENCIA CRÍTICA:\n\n¿Confirma el purgado total de los archivos históricos CSV? Esta acción no se puede deshacer y los datos de tendencias se perderán de forma permanente."); if (!confirmacion) return; try { const response = await fetch('/api/history/clear', { method: 'POST', headers: getApiHeaders() }); if (!response.ok) throw new Error("Fallo en el purgado de archivos"); // Limpia los datasets existentes sin recrear las gráficas. historicalCharts.forEach(config => { const existingChart = chartInstances.get(config.id); if (existingChart) { existingChart.data.labels = []; existingChart.data.datasets.forEach(dataset => dataset.data = []); existingChart.update("none"); } setChartAvailability(config, false); }); alert("Purgado de memoria completado exitosamente."); } catch (error) { console.error("Error ejecutando el borrado:", error); alert("Error de sistema al intentar purgar los archivos."); } };