From 5fc3437ce2bbfe7d52c31db2178fa931eca98b76 Mon Sep 17 00:00:00 2001 From: Exequiel Aramburu Date: Tue, 29 Sep 2026 23:54:01 -0300 Subject: [PATCH] Se incopora las estadisticas nuevamente --- package.json | 3 +- src/gui/controlador/funcionesgenerales.js | 141 +------- src/gui/index.html | 7 +- src/gui/view/modelo_estadisticas.html | 45 +-- src/gui_common/controlador/admingui.js | 1 + src/gui_common/css/style.css | 15 + src/gui_common/js/estadisticas.js | 170 +++++++++ src/gui_web/controlador/funcionesgenerales.js | 324 +----------------- src/gui_web/index.html | 7 +- src/gui_web/view/modelo_estadisticas.html | 49 +-- src/logs/debug.log | 18 + test/estadisticas.test.cjs | 65 ++++ test/ui-smoke.cjs | 37 +- 13 files changed, 341 insertions(+), 541 deletions(-) create mode 100644 src/gui_common/js/estadisticas.js create mode 100644 test/estadisticas.test.cjs diff --git a/package.json b/package.json index cccad93..cacea5c 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "desktop:dev": "node src/tools/nw.cjs dev", "test:config": "node --test test/configuracion.test.cjs", "test:ui": "node test/ui-smoke.cjs", - "test:startup": "node test/ui-startup.cjs" + "test:startup": "node test/ui-startup.cjs", + "test:estadisticas": "node --test test/estadisticas.test.cjs" }, "devDependencies": { "chai": "^4.3.4", diff --git a/src/gui/controlador/funcionesgenerales.js b/src/gui/controlador/funcionesgenerales.js index 1820115..3588d90 100644 --- a/src/gui/controlador/funcionesgenerales.js +++ b/src/gui/controlador/funcionesgenerales.js @@ -1272,139 +1272,10 @@ function VerRegistrosLog(select) }); } -async function graficoEstadisticas() - { - // GRAFICO DEL TIEMPO CHARTJS - document.getElementById("grafico").style.display = "none"; - document.getElementById("actualizando").style.display = "block"; - - //Se genera el grafico normal - if(['Recolector_USB', 'Recolector_IP'].includes(configGeneralEMA.ConfGeneral.Modo)) // se comprueba el modo se encuentra en Recolector - { - - fsconf.readFile('./logs/registro_diario.log', { - encoding: 'utf8', - autoClose: true - }, (err, data) => { - if (err) { - EMA.RegistrarLog("1|ERROR|Error al intentar leer el registro diario para generar el grafico estadistico."); -} else { - var db = new Date(); - var mes = db.getMonth() + 1; if(mes <= 9) {mes = '0'+ mes}; - var dia= db.getDate();if(dia <= 9) {dia = '0'+ dia}; - var fecha = dia + "/" + mes + "/" + db.getFullYear(); - db = null;mes=null;dia=null; - var paraPendientes = fsconf.readFileSync('./logs/debug.log', 'utf8'); - var paraPendientes = paraPendientes.split('\n'); - var paraPendientesALM = paraPendientes.filter(function(datos) {var Resultado = datos.split("|"); return (Resultado[1] == 'PENDIENTE_ALM' && fecha == datos.substring(0,10)); }); - var paraPendientesPUB = paraPendientes.filter(function(datos) {var Resultado = datos.split("|"); return (Resultado[1] == 'PENDIENTE_PUB' && fecha == Resultado[2].substring(15,25)); }); - var CantiddPendientesAlmacenados = paraPendientesALM.length; - var CantiddPendientesPublicados = paraPendientesPUB.length; - paraPendientes = null;paraPendientesPUB = null; paraPendientesALM = null; fecha = null; - var RegistroDiarioTmp = JSON.parse(data); - var CantRegistros = RegistroDiarioTmp.length; - // console.log(CantRegistros); - if(CantRegistros > 1) - { - delete RegistroDiarioTmp['fechaISO']; - delete RegistroDiarioTmp['fechaUTC']; - delete RegistroDiarioTmp['luxer_intencidad_1hs']; - delete RegistroDiarioTmp['viento_direccion_nombre']; - delete RegistroDiarioTmp['lluvia_acu_diaria']; - delete RegistroDiarioTmp['temperatura_interna_max_dia']; - delete RegistroDiarioTmp['temperatura_interna_min_dia']; - delete RegistroDiarioTmp['humedad_interna_max_dia']; - delete RegistroDiarioTmp['humedad_interna_min_dia']; - delete RegistroDiarioTmp['temperatura_externa_max_dia']; - delete RegistroDiarioTmp['temperatura_externa_min_dia']; - delete RegistroDiarioTmp['humedad_externa_max_dia']; - delete RegistroDiarioTmp['humedad_externa_min_dia']; - delete RegistroDiarioTmp['punto_de_rocio_max_dia']; - delete RegistroDiarioTmp['punto_de_rocio_min_dia']; - delete RegistroDiarioTmp['luxer_intencidad_max_dia']; - delete RegistroDiarioTmp['luxer_intencidad_min_dia']; - delete RegistroDiarioTmp['luxer_uv_max_dia']; - delete RegistroDiarioTmp['luxer_uv_min_dia']; - delete RegistroDiarioTmp['presion_relativa_max_dia']; - delete RegistroDiarioTmp['presion_relativa_min_dia']; - delete RegistroDiarioTmp['viento_direccion_prom_dia']; - delete RegistroDiarioTmp['viento_velocidad_max_dia']; - delete RegistroDiarioTmp['viento_rafagas_max_dia']; - RegistroDiarioConCambios = []; - for (var j = 0; j < CantRegistros; j++) { - if ( RegistroDiarioTmp[j].cambio == true) RegistroDiarioConCambios.push(RegistroDiarioTmp[j]); - } - - delete RegistroDiarioConCambios['diferencia']; - // Se generan los datos para el grafico - var fecha_maxima = RegistroDiarioConCambios[RegistroDiarioConCambios.length-1].fecha; - var labels = RegistroDiarioConCambios.map(function(e) { return e.fecha.substring(11, 16) || 0;}); - var tmp_Ext = RegistroDiarioConCambios.map(function(e) { return e.temperatura_externa || 0;}); - var hum_Ext = RegistroDiarioConCambios.map(function(e) { return e.humedad_externa || 0;}); - var tmp_Int = RegistroDiarioConCambios.map(function(e) { return e.temperatura_interna || 0;}); - var hum_Int = RegistroDiarioConCambios.map(function(e) { return e.humedad_interna || 0;}); - var vientos = RegistroDiarioConCambios.map(function(e) { return e.viento_velocidad || 0;}); - var vinetosRaf = RegistroDiarioConCambios.map(function(e) { return e.viento_rafagas || 0;}); - var UV = RegistroDiarioConCambios.map(function(e) { return e.luxer_uv || 0;}); - var Luxer = RegistroDiarioConCambios.map(function(e) { return e.luxer_intencidad || 0;}); - var Lluvia = RegistroDiarioConCambios.map(function(e) { return e.lluvia_acu_hora || 0;}); - var presionRel = RegistroDiarioConCambios.map(function(e) { return e.presion_relativa || 0;}); - var validacion = RegistroDiarioConCambios.map(function(e) { return (e.estado_validacion_ant) || 0;}); - var puntorocio = RegistroDiarioConCambios.map(function(e) { return e.punto_de_rocio || 0;}); - // GRAFICO RESUMEN CON RGRAPH - var resumenLabels = [ - 'Temp.Ext', 'Hum.Ext', 'Temp.Int', 'Hum.Int', 'Viento', 'Lluvia', 'Pres.Rel', 'UV', 'Luxer', 'Pto.Rocío', 'Valid.' - ]; - var resumenData = [ - tmp_Ext[tmp_Ext.length-1] || 0, - hum_Ext[hum_Ext.length-1] || 0, - tmp_Int[tmp_Int.length-1] || 0, - hum_Int[hum_Int.length-1] || 0, - vientos[vientos.length-1] || 0, - Lluvia[Lluvia.length-1] || 0, - presionRel[presionRel.length-1] || 0, - UV[UV.length-1] || 0, - Luxer[Luxer.length-1] || 0, - puntorocio[puntorocio.length-1] || 0, - validacion[validacion.length-1] || 0 - ]; - - - window._GraphInicioChart = new RGraph.Bar({ - id: 'graph_inicio', - data: resumenData, - options: { - colors: ['#1e90ff','#00b894','#fdcb6e','#e17055','#636e72','#00bfff','#6c5ce7','#fd79a8','#ffeaa7','#fab1a0','#636e72'], - backgroundGrid: true, - xaxisLabels: resumenLabels, - yaxis: true, - yaxisLabelsCount: 5, - marginLeft: 50, - marginBottom: 60, - marginTop: 40, - marginRight: 20, - title: 'Resumen Últimos Valores', - titleBold: true - } - }).draw(); - // document.getElementById("actualizando").style.display = "none"; - //console.log("Grafico resumen generado con RGraph."); - //console.table(resumenLabels); - // console.table(resumenData); - - }else{ - return false - //document.getElementById("inicio_estadisticas").innerHTML = "

No hay registros para procesar ..

"; - //document.getElementById("grafico").innerHTML = "

No hay registros para procesar ..

"; - //document.getElementById("actualizando").style.display = "none"; - } - } - }); - - }else{ //MODO Visor - return false; - //document.getElementById("inicio_estadisticas").innerHTML = "

Las estadisticas solo en modo recolector.

"; - //document.getElementById("grafico").innerHTML = "

Gráficos de estadisticas solo en modo recolector.

"; - //document.getElementById("actualizando").style.display = "none"; - } +async function graficoEstadisticas() { + return EstadisticasUI.mount(document.getElementById('estadisticas'), { + mode: configGeneralEMA.ConfGeneral.Modo, + system: configGeneralEMA.ConfGeneral.SistemaMetrico, + load: () => new Promise((resolve, reject) => fsconf.readFile('./logs/registro_diario.log', 'utf8', (err, data) => err ? reject(err) : resolve(data))) + }); } \ No newline at end of file diff --git a/src/gui/index.html b/src/gui/index.html index dbf09e6..ee48cee 100644 --- a/src/gui/index.html +++ b/src/gui/index.html @@ -23,6 +23,7 @@ + @@ -125,9 +126,9 @@ - + @@ -143,7 +144,7 @@
- +
diff --git a/src/gui/view/modelo_estadisticas.html b/src/gui/view/modelo_estadisticas.html index 6dbbb27..a111a8c 100644 --- a/src/gui/view/modelo_estadisticas.html +++ b/src/gui/view/modelo_estadisticas.html @@ -1,38 +1,7 @@ - -
- - -

-
-


Cargando Gráfico... -
-
- -
- -
- -
-
- - \ No newline at end of file +
+
+

Estadísticas del registro diario

+ +
+
+
diff --git a/src/gui_common/controlador/admingui.js b/src/gui_common/controlador/admingui.js index 2aca5fe..44ed8c9 100644 --- a/src/gui_common/controlador/admingui.js +++ b/src/gui_common/controlador/admingui.js @@ -876,6 +876,7 @@ function MenuConfig () { ConfigUI.visibility(); } async function MenuGUI (elemento) { if (!ConfigUI.canLeave()) return; + EstadisticasUI.dispose(); try { document.getElementById("actualizando_inicio").hidden = false; document.getElementById("actualizando_inicio").style.display = "block"; diff --git a/src/gui_common/css/style.css b/src/gui_common/css/style.css index 4d3688d..366b0c9 100644 --- a/src/gui_common/css/style.css +++ b/src/gui_common/css/style.css @@ -568,3 +568,18 @@ table.dataTable.table-bordered { } .notifyjs-arrow { display: none !important; } } + +/* Mantener legibles los valores y el historial sobre los fondos de día/noche. */ +.estadisticas-panel { + background: rgba(20, 34, 50, 0.96); + color: #f2f5f9; + padding: 1rem; + border-radius: 0.5rem; +} +.estadisticas-panel .table { + --bs-table-bg: transparent; + --bs-table-color: #f2f5f9; + color: #f2f5f9; +} +.estadisticas-panel caption { color: #d0d9e5; } +.estadisticas-panel .form-label { color: #f2f5f9; } diff --git a/src/gui_common/js/estadisticas.js b/src/gui_common/js/estadisticas.js new file mode 100644 index 0000000..b677941 --- /dev/null +++ b/src/gui_common/js/estadisticas.js @@ -0,0 +1,170 @@ +/* Estadísticas del registro diario, compartidas por NW.js y la interfaz web. */ +const EstadisticasUI = (() => { + const variables = [ + ['temperatura_externa', 'Temperatura exterior', '°C', 'temperature'], + ['temperatura_interna', 'Temperatura interior', '°C', 'temperature'], + ['humedad_externa', 'Humedad exterior', '%'], + ['humedad_interna', 'Humedad interior', '%'], + ['punto_de_rocio', 'Punto de rocío', '°C', 'temperature'], + ['viento_velocidad', 'Velocidad del viento', 'km/h', 'speed'], + ['viento_rafagas', 'Ráfagas', 'km/h', 'speed'], + ['presion_relativa', 'Presión relativa', 'hPa'], + ['lluvia_acu_hora', 'Lluvia acumulada en una hora', 'mm'], + ['lluvia_acu_diaria', 'Lluvia acumulada diaria', 'mm'], + ['luxer_uv', 'Índice UV', ''], + ['luxer_intencidad', 'Iluminación', 'lux'] + ]; + let generation = 0; + function numeric(value) { + if (typeof value !== 'number' && typeof value !== 'string') return null; + if (typeof value === 'string' && !value.trim()) return null; + const number = Number(value); + return Number.isFinite(number) ? number : null; + } + function timestamp(record) { + const iso = Date.parse(record.fechaISO); + if (Number.isFinite(iso)) return iso; + const parts = String(record.fecha || '').match(/^(\d{2})\/(\d{2})\/(\d{4})[ T](\d{2}):(\d{2})(?::(\d{2}))?$/); + return parts ? new Date(+parts[3], +parts[2] - 1, +parts[1], +parts[4], +parts[5], +(parts[6] || 0)).getTime() : NaN; + } + function analyze(input, system = 'SI') { + const records = typeof input === 'string' ? JSON.parse(input) : input; + if (!Array.isArray(records)) throw new Error('El registro diario no contiene una lista de mediciones.'); + const rows = records.filter(r => r && typeof r === 'object' && !Array.isArray(r)) + .map(record => ({record, time: timestamp(record)})) + .filter(row => Number.isFinite(row.time)).sort((a, b) => a.time - b.time); + const series = variables.map(([key, label, unit, kind]) => { + const imperial = system !== 'SI'; + const points = rows.map(({record, time}) => { + let value = numeric(record[key]); + if (value !== null && imperial) { + if (kind === 'temperature') value = value * 9 / 5 + 32; + if (kind === 'speed') value *= 0.621371192; + } + return {time, value, label: String(record.fecha || record.fechaISO)}; + }); + const values = points.map(p => p.value).filter(v => v !== null); + // No sumar acumulados de lluvia ni convertir ausencias en ceros. + const stats = values.reduce((acc, v) => ({min: Math.min(acc.min, v), max: Math.max(acc.max, v), sum: acc.sum + v}), {min: Infinity, max: -Infinity, sum: 0}); + return {key, label, unit: imperial && kind === 'temperature' ? '°F' : imperial && kind === 'speed' ? 'mph' : unit, + points, count: values.length, min: values.length ? stats.min : null, + max: values.length ? stats.max : null, mean: values.length ? stats.sum / values.length : null, + last: points.length ? points[points.length - 1].value : null}; + }); + return {total: records.length, dated: rows.length, discarded: records.length - rows.length, + changed: rows.filter(r => r.record.cambio === true).length, series, + first: rows[0]?.record.fecha || rows[0]?.record.fechaISO, + last: rows.at(-1)?.record.fecha || rows.at(-1)?.record.fechaISO}; + } + const format = value => value === null ? '—' : value.toLocaleString('es-AR', {maximumFractionDigits: 2}); + function element(tag, text, className) { + const node = document.createElement(tag); + if (text !== undefined) node.textContent = text; + if (className) node.className = className; + return node; + } + function draw(target, series) { + target.replaceChildren(); + if (!series.count) { target.append(element('p', 'No hay mediciones válidas para esta variable.')); return; } + const svgNS = 'http://www.w3.org/2000/svg'; + function svgNode(tag, attrs, text) { + const node = document.createElementNS(svgNS, tag); + for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, value); + if (text !== undefined) node.textContent = text; + return node; + } + const svg = svgNode('svg', {viewBox: '0 0 900 320', role: 'img', 'aria-label': series.label + ' (' + series.unit + ')', style: 'width:100%;max-height:420px;min-height:220px'}); + svg.append(svgNode('title', {}, series.label + ' · ' + series.unit)); + const first = series.points[0].time, last = series.points.at(-1).time; + const span = series.max - series.min || Math.max(Math.abs(series.max) * 0.1, 1); + const low = series.min - span * 0.1, high = series.max + span * 0.1; + const x = t => first === last ? 470 : 80 + (t - first) / (last - first) * 780; + const y = v => 265 - (v - low) / (high - low) * 240; + for (let i = 0; i <= 4; i++) { + const value = low + (high - low) * i / 4; + svg.append(svgNode('line', {x1:80, x2:860, y1:y(value), y2:y(value), stroke:'currentColor', opacity:0.2})); + svg.append(svgNode('text', {x:72, y:y(value) + 4, 'text-anchor':'end', fill:'currentColor', 'font-size':13}, format(value))); + } + let path = '', gap = true; + for (const point of series.points) { + if (point.value === null) { gap = true; continue; } + path += `${gap ? 'M' : 'L'}${x(point.time).toFixed(2)},${y(point.value).toFixed(2)} `; + gap = false; + } + svg.append(svgNode('path', {d:path, fill:'none', stroke:'#64b5ff', 'stroke-width':2, 'vector-effect':'non-scaling-stroke'})); + // Limitar marcadores DOM para históricos grandes, manteniendo la curva completa. + const valid = series.points.filter(p => p.value !== null); + const step = Math.max(1, Math.ceil(valid.length / 200)); + valid.forEach((point, i) => { + if (i % step && i !== valid.length - 1) return; + const dot = svgNode('circle', {cx:x(point.time), cy:y(point.value), r:3, fill:'#64b5ff'}); + dot.append(svgNode('title', {}, `${point.label}: ${format(point.value)} ${series.unit}`)); + svg.append(dot); + }); + svg.append(svgNode('text', {x:80, y:300, fill:'currentColor', 'font-size':12}, series.points[0].label)); + svg.append(svgNode('text', {x:860, y:300, 'text-anchor':'end', fill:'currentColor', 'font-size':12}, series.points.at(-1).label)); + target.append(svg); + } + function render(target, result) { + target.replaceChildren(); + if (!result.dated) { + target.append(element('p', result.total ? 'No hay registros con fecha válida para procesar.' : 'Todavía no hay registros diarios. Iniciá la recolección y actualizá esta pantalla.', 'alert alert-info')); + return; + } + target.append(element('p', `${result.dated} registros · ${result.changed} con cambios · ${result.first} — ${result.last}`)); + if (result.discarded) target.append(element('p', `Se omitieron ${result.discarded} registros sin fecha válida.`, 'alert alert-warning')); + const tableBox = element('div', undefined, 'table-responsive'); + const table = element('table', undefined, 'table table-bordered table-sm'); + table.append(element('caption', 'Resumen de las muestras disponibles. Los acumulados de lluvia no se suman. — indica un dato ausente.')); + const header = element('tr'); + for (const label of ['Variable', 'Unidad', 'Muestras', 'Mínimo', 'Promedio', 'Máximo', 'Último']) { const th = element('th', label); th.scope = 'col'; header.append(th); } + const head = element('thead'); head.append(header); table.append(head); + const body = element('tbody'); + for (const series of result.series) { + const row = element('tr'); + for (const value of [series.label, series.unit, series.count, format(series.min), format(series.mean), format(series.max), format(series.last)]) row.append(element('td', value)); + body.append(row); + } + table.append(body); tableBox.append(table); target.append(tableBox); + const label = element('label', 'Variable del gráfico', 'form-label'); label.htmlFor = 'estadisticas-variable'; + const select = element('select', undefined, 'form-select mb-3'); select.id = label.htmlFor; + for (const series of result.series) { + const option = element('option', `${series.label} (${series.unit || 'índice'})`); option.value = series.key; select.append(option); + } + select.value = (result.series.find(s => s.count) || result.series[0]).key; + const graph = element('div'); graph.dataset.estadisticasGraph = ''; + select.onchange = () => draw(graph, result.series.find(s => s.key === select.value)); + target.append(label, select, graph); select.onchange(); + } + async function mount(root, {mode, system = 'SI', load}) { + if (!root) return; + const id = ++generation; + const target = root.querySelector('[data-estadisticas-content]'); + const button = root.querySelector('[data-estadisticas-refresh]'); + if (!target || !button) return; + button.onclick = () => mount(root, {mode, system, load}); + if (!['Recolector', 'Recolector_USB', 'Recolector_IP'].includes(mode)) { + button.disabled = true; + const notice = element('p', 'Las estadísticas del registro diario están disponibles en modo recolector.', 'alert alert-info'); + notice.style.setProperty('--bs-alert-bg', '#1a606f'); + notice.style.setProperty('--bs-alert-color', '#fff'); + target.replaceChildren(notice); + return; + } + button.disabled = true; + target.setAttribute('aria-busy', 'true'); + target.replaceChildren(element('p', 'Cargando estadísticas…', 'alert alert-info')); + try { + const result = analyze(await load(), system); + if (id === generation && root.isConnected) render(target, result); + } catch (error) { + if (id === generation && root.isConnected) target.replaceChildren(element('p', 'No se pudieron cargar las estadísticas. Revisá el registro diario y volvé a intentar.', 'alert alert-danger')); + console.warn('Estadísticas:', error.message); + } finally { + if (id === generation) { button.disabled = false; target.setAttribute('aria-busy', 'false'); } + } + } + function dispose() { generation++; } + return {analyze, mount, dispose}; +})(); +if (typeof module === 'object' && module.exports) module.exports = EstadisticasUI; diff --git a/src/gui_web/controlador/funcionesgenerales.js b/src/gui_web/controlador/funcionesgenerales.js index 59a2b31..5d666a5 100644 --- a/src/gui_web/controlador/funcionesgenerales.js +++ b/src/gui_web/controlador/funcionesgenerales.js @@ -1257,322 +1257,10 @@ function VerRegistrosLog(select) }); } -async function graficoEstadisticas() -{ - // GRAFICO DEL TIEMPO CHARTJS - document.getElementById("grafico").style.display = "none"; - document.getElementById("actualizando").style.display = "block"; - if(configGeneralEMA.ConfGeneral.Modo == 'Recolector') // se comprueba el modo se encuentra en Recolector - { - //Se genera el grafico - $.get("/verHistorico", function(data, status) - { - var db = new Date(); - var mes = db.getMonth() + 1; if(mes <= 9) {mes = '0'+ mes}; - var dia= db.getDate();if(dia <= 9) {dia = '0'+ dia}; - var fecha = dia + "/" + mes + "/" + db.getFullYear(); - db = null;mes=null;dia=null; - var RegistroDiarioTmp = JSON.parse(data); - var CantRegistros = RegistroDiarioTmp.length; - if(CantRegistros > 1) - { - delete RegistroDiarioTmp['fechaISO']; - delete RegistroDiarioTmp['fechaUTC']; - delete RegistroDiarioTmp['luxer_intencidad_1hs']; - delete RegistroDiarioTmp['viento_direccion_nombre']; - delete RegistroDiarioTmp['lluvia_acu_diaria']; - delete RegistroDiarioTmp['temperatura_interna_max_dia']; - delete RegistroDiarioTmp['temperatura_interna_min_dia']; - delete RegistroDiarioTmp['humedad_interna_max_dia']; - delete RegistroDiarioTmp['humedad_interna_min_dia']; - delete RegistroDiarioTmp['temperatura_externa_max_dia']; - delete RegistroDiarioTmp['temperatura_externa_min_dia']; - delete RegistroDiarioTmp['humedad_externa_max_dia']; - delete RegistroDiarioTmp['humedad_externa_min_dia']; - delete RegistroDiarioTmp['punto_de_rocio_max_dia']; - delete RegistroDiarioTmp['punto_de_rocio_min_dia']; - delete RegistroDiarioTmp['luxer_intencidad_max_dia']; - delete RegistroDiarioTmp['luxer_intencidad_min_dia']; - delete RegistroDiarioTmp['luxer_uv_max_dia']; - delete RegistroDiarioTmp['luxer_uv_min_dia']; - delete RegistroDiarioTmp['presion_relativa_max_dia']; - delete RegistroDiarioTmp['presion_relativa_min_dia']; - delete RegistroDiarioTmp['viento_direccion_prom_dia']; - delete RegistroDiarioTmp['viento_velocidad_max_dia']; - delete RegistroDiarioTmp['viento_rafagas_max_dia']; - RegistroDiarioConCambios = []; - for (var j = 0; j < CantRegistros; j++) { - if ( RegistroDiarioTmp[j].cambio == true) RegistroDiarioConCambios.push(RegistroDiarioTmp[j]); - } - - delete RegistroDiarioConCambios['diferencia']; - // Se generan los datos para el grafico - var fecha_maxima = RegistroDiarioConCambios[RegistroDiarioConCambios.length-1].fecha; - var labels = RegistroDiarioConCambios.map(function(e) { return e.fecha.substring(11, 16) || 0;}); - var tmp_Ext = RegistroDiarioConCambios.map(function(e) { return e.temperatura_externa || 0;}); - var hum_Ext = RegistroDiarioConCambios.map(function(e) { return e.humedad_externa || 0;}); - var tmp_Int = RegistroDiarioConCambios.map(function(e) { return e.temperatura_interna || 0;}); - var hum_Int = RegistroDiarioConCambios.map(function(e) { return e.humedad_interna || 0;}); - var vientos = RegistroDiarioConCambios.map(function(e) { return e.viento_velocidad || 0;}); - var vinetosRaf = RegistroDiarioConCambios.map(function(e) { return e.viento_rafagas || 0;}); - var UV = RegistroDiarioConCambios.map(function(e) { return e.luxer_uv || 0;}); - var Luxer = RegistroDiarioConCambios.map(function(e) { return e.luxer_intencidad || 0;}); - var Lluvia = RegistroDiarioConCambios.map(function(e) { return e.lluvia_acu_hora || 0;}); - var presionRel = RegistroDiarioConCambios.map(function(e) { return e.presion_relativa || 0;}); - var validacion = RegistroDiarioConCambios.map(function(e) { return (e.estado_validacion_ant) || 0;}); - var puntorocio = RegistroDiarioConCambios.map(function(e) { return e.punto_de_rocio || 0;}); - var totalErrores = 0; - for(var i=0;i $.ajax({url: '/VerHistorico', dataType: 'json', timeout: 15000}) + }); } \ No newline at end of file diff --git a/src/gui_web/index.html b/src/gui_web/index.html index ee30def..52c1407 100644 --- a/src/gui_web/index.html +++ b/src/gui_web/index.html @@ -21,6 +21,7 @@ + @@ -108,9 +109,9 @@ - + @@ -126,7 +127,7 @@
- +
diff --git a/src/gui_web/view/modelo_estadisticas.html b/src/gui_web/view/modelo_estadisticas.html index a2396fd..a111a8c 100644 --- a/src/gui_web/view/modelo_estadisticas.html +++ b/src/gui_web/view/modelo_estadisticas.html @@ -1,42 +1,7 @@ - -
- - -

-
-


Cargando Gráfico... -
-
- -
- - - - - -
- - \ No newline at end of file +
+
+

Estadísticas del registro diario

+ +
+
+
diff --git a/src/logs/debug.log b/src/logs/debug.log index eb77b0a..b9036fb 100644 --- a/src/logs/debug.log +++ b/src/logs/debug.log @@ -39,3 +39,21 @@ 29/09/2026 01:59:26 - 0|CORE|✅ Modulo node-hid cargado correctamente. 29/09/2026 01:59:26 - 0|CORE|⏳ Iniciando Ema Libre en Modo gráfico o Web. 29/09/2026 01:59:43 - 0|CORE|🔚 SALIR: Se cerro el core de EMA LIBRE. Hasta luego!!!. +29/09/2026 23:41:13 - 0|CORE|🕵️. Buscando Modulo USB para el S.O - linux_x64_gui +29/09/2026 23:41:13 - 0|CORE|✅ Modulo node-hid cargado correctamente. +29/09/2026 23:41:13 - 0|CORE|⏳ Iniciando Ema Libre en Modo gráfico o Web. +29/09/2026 23:41:52 - 0|CORE|⚙️ RECONFIGURANDO: Se reconfigura el recolectorde EMA LIBRE. +29/09/2026 23:42:49 - 0|CORE|⚙️ RECONFIGURANDO: Se reconfigura el recolectorde EMA LIBRE. +29/09/2026 23:50:04 - 0|CORE|🕵️. Buscando Modulo USB para el S.O - linux_x64_gui +29/09/2026 23:50:04 - 0|CORE|✅ Modulo node-hid cargado correctamente. +29/09/2026 23:50:04 - 0|CORE|⏳ Iniciando Ema Libre en Modo gráfico o Web. +29/09/2026 23:50:16 - 0|CORE|🕵️. Buscando Modulo USB para el S.O - linux_x64_gui +29/09/2026 23:50:16 - 0|CORE|✅ Modulo node-hid cargado correctamente. +29/09/2026 23:50:16 - 0|CORE|⏳ Iniciando Ema Libre en Modo gráfico o Web. +29/09/2026 23:51:10 - 0|CORE|🕵️. Buscando Modulo USB para el S.O - linux_x64_gui +29/09/2026 23:51:10 - 0|CORE|✅ Modulo node-hid cargado correctamente. +29/09/2026 23:51:10 - 0|CORE|⏳ Iniciando Ema Libre en Modo gráfico o Web. +29/09/2026 23:51:19 - 0|CORE|🔚 SALIR: Se cerro el core de EMA LIBRE. Hasta luego!!!. +29/09/2026 23:52:16 - 0|CORE|🕵️. Buscando Modulo USB para el S.O - linux_x64_gui +29/09/2026 23:52:16 - 0|CORE|✅ Modulo node-hid cargado correctamente. +29/09/2026 23:52:16 - 0|CORE|⏳ Iniciando Ema Libre en Modo gráfico o Web. diff --git a/test/estadisticas.test.cjs b/test/estadisticas.test.cjs new file mode 100644 index 0000000..85fa5ba --- /dev/null +++ b/test/estadisticas.test.cjs @@ -0,0 +1,65 @@ +const {test} = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const vm = require('node:vm'); +const stats = require('../src/gui_common/js/estadisticas.js'); +const row = (time, values = {}) => ({fecha: `29/09/2026 ${time}`, ...values}); +const metric = (result, key = 'temperatura_externa') => result.series.find(s => s.key === key); + +test('empty history and malformed input', () => { + assert.equal(stats.analyze('[]').dated, 0); + assert.throws(() => stats.analyze('{}')); + assert.throws(() => stats.analyze('invalid')); +}); +test('one unchanged record is enough', () => { + const result = stats.analyze([row('12:00:00', {temperatura_externa: 0, cambio: false})]); + assert.equal(result.changed, 0); + assert.equal(metric(result).mean, 0); + assert.equal(metric(result).count, 1); +}); +test('missing values do not become zero and latest remains missing', () => { + const result = stats.analyze([row('12:00:00', {temperatura_externa: '-4'}), row('12:01:00', {temperatura_externa: 0}), row('12:02:00', {temperatura_externa: ''}), row('12:03:00', {temperatura_externa: null})]); + assert.equal(metric(result).mean, -2); + assert.equal(metric(result).count, 2); + assert.equal(metric(result).last, null); + assert.deepEqual(metric(result).points.map(p => p.value), [-4, 0, null, null]); +}); +test('orders dates and excludes malformed rows without mutating input', () => { + const input = [row('13:00:00', {temperatura_externa: 20}), null, {}, row('12:00:00', {temperatura_externa: 10, cambio: true})]; + const before = JSON.stringify(input); + const result = stats.analyze(input); + assert.equal(result.discarded, 2); + assert.equal(result.changed, 1); + assert.equal(metric(result).last, 20); + assert.equal(result.first, '29/09/2026 12:00:00'); + assert.equal(JSON.stringify(input), before); +}); +test('imperial temperature and wind conversion preserves absent measurements', () => { + const result = stats.analyze([row('12:00:00', {temperatura_externa: 0, viento_velocidad: 100})], 'SM'); + assert.equal(metric(result).mean, 32); + assert.equal(metric(result).unit, '°F'); + assert.equal(metric(result, 'viento_velocidad').unit, 'mph'); + assert.ok(Math.abs(metric(result, 'viento_velocidad').mean - 62.1371192) < 0.000001); + assert.equal(metric(result, 'temperatura_interna').mean, null); +}); +test('rain accumulators are not summed', () => { + const result = stats.analyze([row('12:00:00', {lluvia_acu_diaria: 10}), row('13:00:00', {lluvia_acu_diaria: 11})]); + assert.equal(metric(result, 'lluvia_acu_diaria').last, 11); + assert.equal(metric(result, 'lluvia_acu_diaria').max, 11); +}); +test('both production adapters load the proper data source', async () => { + for (const mode of ['gui', 'gui_web']) { + let request; + const sandbox = { + configGeneralEMA: {ConfGeneral: {Modo: 'Recolector_IP', SistemaMetrico: 'SI'}}, + document: {getElementById: id => id}, + EstadisticasUI: {mount: async (root, options) => {assert.equal(root, 'estadisticas'); assert.equal(options.mode, 'Recolector_IP'); return options.load();}}, + fsconf: {readFile: (file, encoding, cb) => {request = file; cb(null, '[]');}}, + $: {ajax: options => {request = options.url; return Promise.resolve('[]');}} + }; + const script = fs.readFileSync(`src/${mode}/controlador/funcionesgenerales.js`, 'utf8').split('async function graficoEstadisticas()')[1]; + vm.runInNewContext('async function graficoEstadisticas()' + script, sandbox); + assert.equal(await sandbox.graficoEstadisticas(), '[]'); + assert.equal(request, mode === 'gui' ? './logs/registro_diario.log' : '/VerHistorico'); + } +}); diff --git a/test/ui-smoke.cjs b/test/ui-smoke.cjs index ac5fe6b..1da8931 100644 --- a/test/ui-smoke.cjs +++ b/test/ui-smoke.cjs @@ -36,6 +36,41 @@ async function probe() { check(document.querySelector(tab.dataset.bsTarget)?.classList.contains('active'), 'Tab failed: ' + tab.id); } tests.push('all navigation tabs'); + const statsRoot = document.getElementById('estadisticas'); + statsRoot.innerHTML = window.statisticsHTML; + const records = [ + {fecha:'29/09/2026 12:00:00', temperatura_externa:0, cambio:false}, + {fecha:'29/09/2026 12:02:00', temperatura_externa:10, cambio:true} + ]; + await EstadisticasUI.mount(statsRoot, {mode:'Recolector_IP', load:async () => JSON.stringify(records)}); + check(statsRoot.querySelectorAll('tbody tr').length === 12, 'Statistics summary missing'); + check(statsRoot.querySelector('svg path').getAttribute('d').includes('L'), 'History line missing'); + document.getElementById('estadisticas-tab').click(); + nw.Window.get().show(); + statsRoot.classList.add('ema-panel'); + statsRoot.style.padding = '16px'; + await wait(400); + check(statsRoot.getBoundingClientRect().height > 300, 'Statistics panel not visible'); + const capture = await new Promise(resolve => nw.Window.get().capturePage(resolve, {format:'png', datatype:'buffer'})); + require('fs').writeFileSync(window.resultFile + '.estadisticas.png', capture); + const variable = statsRoot.querySelector('select'); + variable.value = 'humedad_externa'; variable.dispatchEvent(new Event('change')); + check(!statsRoot.querySelector('svg'), 'Missing values plotted as zeros'); + await EstadisticasUI.mount(statsRoot, {mode:'Recolector_USB', load:async () => [records[0]]}); + check(statsRoot.querySelectorAll('svg circle').length === 1, 'Single unchanged record not plotted'); + await EstadisticasUI.mount(statsRoot, {mode:'Recolector', load:async () => []}); + check(statsRoot.textContent.includes('Todavía no hay registros'), 'Empty state missing'); + await EstadisticasUI.mount(statsRoot, {mode:'Recolector', load:async () => {throw Error('fixture failure');}}); + check(statsRoot.querySelector('.alert-danger') && !statsRoot.querySelector('button').disabled, 'Error/retry state missing'); + await EstadisticasUI.mount(statsRoot, {mode:'Visor', load:async () => {throw Error('Visor must not load');}}); + check(statsRoot.textContent.includes('modo recolector'), 'Visor message missing'); + let resolveOld; + const old = EstadisticasUI.mount(statsRoot, {mode:'Recolector', load:() => new Promise(resolve => {resolveOld = resolve;})}); + await EstadisticasUI.mount(statsRoot, {mode:'Recolector', load:async () => []}); + resolveOld(records); await old; + check(!statsRoot.querySelector('svg'), 'Stale response overwrote newer statistics'); + tests.push('statistics summary, chart, selection, single record, empty/error/visor states and stale requests'); + $('#settings').html(window.settingsHTML); const fixture = window.configFixture; const interfaces = {eth0: [{family:'IPv4',internal:false,address:'127.0.0.1'}]}; @@ -154,7 +189,7 @@ window.persistConfiguration=async draft => { else {const response=await $.ajax({url:'/GuardarConf',method:'POST',contentType:'application/json',data:JSON.stringify({configuracion:draft})});if(!response.ok)throw new Error('Save failed');} }; window.readConfiguration=()=>{const sandbox={module:{exports:{}}};require('vm').runInNewContext(require('fs').readFileSync(${JSON.stringify(configFile)},'utf8'),sandbox);return sandbox.module.exports;}; -window.settingsHTML=${JSON.stringify(settings)};window.languageURL=${JSON.stringify(language)};window.resultFile=${JSON.stringify(resultFile)};addEventListener('load',${probe.toString()});`); +window.statisticsHTML=${JSON.stringify(read(mode + '/view/modelo_estadisticas.html'))};window.settingsHTML=${JSON.stringify(settings)};window.languageURL=${JSON.stringify(language)};window.resultFile=${JSON.stringify(resultFile)};addEventListener('load',${probe.toString()});`); const args = JSON.parse(read('package.json'))['chromium-args'].replace('--disable-logging', ''); fs.writeFileSync(path.join(folder, 'package.json'), JSON.stringify({name:'carpincho-ui-test',main:'index.html','chromium-args':args,window:{show:false,width:1200,height:900}})); await new Promise((resolve, reject) => {