Cambios generales

This commit is contained in:
Exequiel Aramburu 2026-09-28 21:33:24 -03:00
parent 5221b6fb19
commit 6777bf363b
7 changed files with 147 additions and 108 deletions

View File

@ -1,4 +1,11 @@
** Mejoras de interfaz y configuración (Septiembre 27, 2026) ** Versión 2.0.0: (Septiembre 27, 2026)
+ Se actualiza la versión de NWJS → 0.107.0
+ Node-hid ahora no necesita nw-gyp, se actualiza la compilación
+ Se migra escritorio y web a Bootstrap 5 con Popper incluido en el bundle.
+ Se actualiza bootstrap-select a 1.14.0-beta3 y se reconstruyen selects conservando el valor para evitar opciones duplicadas.
+ Se integran DataTables 1.13.11 y Buttons 2.4.3 con estilos Bootstrap 5.
+ Se corrigen atributos, clases, rutas de imágenes y estilos de tablas/formularios.
+ Se agrega desktop:dev y test:ui (requiere un binario NW.js SDK como argumento).
+ Se actualizó RGraph 7.01 → 7.21 + Se actualizó RGraph 7.01 → 7.21
+ NW.js 0.117.0 centralizado en runtime.json para desarrollo y empaquetado x64. + NW.js 0.117.0 centralizado en runtime.json para desarrollo y empaquetado x64.
+ Formulario compartido con controles nativos, validación y aviso de cambios sin guardar. + Formulario compartido con controles nativos, validación y aviso de cambios sin guardar.
@ -8,15 +15,8 @@
+ Estados separados para conexión, última lectura y publicación, con motivos y acciones. + Estados separados para conexión, última lectura y publicación, con motivos y acciones.
+ Pruebas de configuración, interfaz y arranque aisladas de la estación real. + Pruebas de configuración, interfaz y arranque aisladas de la estación real.
+ El empaquetador ARM histórico queda bloqueado: su Node/NW.js no soportan esta versión. + El empaquetador ARM histórico queda bloqueado: su Node/NW.js no soportan esta versión.
+ Se mejora el iconizado.
** Versión 2.0.0: (Septiembre 27, 2026) + Se mejora el boton de guardado.
+ Se actualiza la versión de NWJS → 0.107.0
+ Node-hid ahora no necesita nw-gyp, se actualiza la compilación
+ Se migra escritorio y web a Bootstrap 5 con Popper incluido en el bundle.
+ Se actualiza bootstrap-select a 1.14.0-beta3 y se reconstruyen selects conservando el valor para evitar opciones duplicadas.
+ Se integran DataTables 1.13.11 y Buttons 2.4.3 con estilos Bootstrap 5.
+ Se corrigen atributos, clases, rutas de imágenes y estilos de tablas/formularios.
+ Se agrega desktop:dev y test:ui (requiere un binario NW.js SDK como argumento).
** Versión 1.36: (Enero 10 , 2026) ** Versión 1.36: (Enero 10 , 2026)
+ Se incorporó recolectar por WIFI/IP. + Se incorporó recolectar por WIFI/IP.

View File

@ -160,7 +160,7 @@
<script> <script>
$(document).ready(function() { $(document).ready(function() {
$('.menu').on('click', function(evnt) { $('.menu').on('click', function(evnt) {
var obj = $(evnt.target); var obj = $(evnt.currentTarget);
$('#' + obj.attr('aria-controls')).empty(); $('#' + obj.attr('aria-controls')).empty();
let screens = gui.Screen.screens; let screens = gui.Screen.screens;
let winX = win.x; let winX = win.x;
@ -178,7 +178,7 @@
} }
} }
//nw.Window.get().moveTo(0); //nw.Window.get().moveTo(0);
switch (obj.attr('id')) { switch (obj.attr('aria-controls')) {
case 'Wzoomin': case 'Wzoomin':
win.zoomLevel += 0.3; win.zoomLevel += 0.3;
break; break;
@ -190,9 +190,7 @@
nw.Window.get().resizeTo(currentScreen.bounds.width, currentScreen.bounds.height); nw.Window.get().resizeTo(currentScreen.bounds.width, currentScreen.bounds.height);
break; break;
case 'Wmin': case 'Wmin':
nw.Window.get().minimize(); ocultarVentana();
//win.hide(); // Oculta completamente la ventana
//win.setShowInTaskbar(false); // Quita el icono del taskbar
break; break;
case 'Wmax': case 'Wmax':
nw.Window.get().moveTo(currentScreen.bounds.x, currentScreen.bounds.y); nw.Window.get().moveTo(currentScreen.bounds.x, currentScreen.bounds.y);

View File

@ -32,6 +32,8 @@
// y exporta las variables necesarias para el resto del archivo. // y exporta las variables necesarias para el resto del archivo.
var configGeneralEMA, configGlobalEMA, funcionesActualizar, EMA, gui, os, win; var configGeneralEMA, configGlobalEMA, funcionesActualizar, EMA, gui, os, win;
var _IS_NW = false; var _IS_NW = false;
let tray = null;
let ventanaMinimizada = false;
const iconoEncendido = './gui_common/iconos/icono_enc.png'; // Imagen del Icono Encendido const iconoEncendido = './gui_common/iconos/icono_enc.png'; // Imagen del Icono Encendido
const iconoApagado = './gui_common/iconos/icono_off.png'; // Imagen del Icono Apagado const iconoApagado = './gui_common/iconos/icono_off.png'; // Imagen del Icono Apagado
const iconoPublicado = './gui_common/iconos/icono_pub.png'; // Imagen del Icono Prublicado --> no utilizada const iconoPublicado = './gui_common/iconos/icono_pub.png'; // Imagen del Icono Prublicado --> no utilizada
@ -58,85 +60,64 @@ if (_IS_NW) {
// **********************Iconizado / Menu Superior ********************************************************* // **********************Iconizado / Menu Superior *********************************************************
gui.App.clearCache(); // Se limpia cache gui.App.clearCache(); // Se limpia cache
win = gui.Window.get(); // Se obtiene la ventana actual win = gui.Window.get(); // Se obtiene la ventana actual
// Crear el icono en el tray // Mantener el icono y su menú durante toda la vida de la ventana.
function createTray() { function mostrarVentana() {
if (tray) return; // Evitar duplicados win.setShowInTaskbar(true);
win.show();
if (ventanaMinimizada) {
win.restore();
ventanaMinimizada = false;
}
win.focus();
}
// Crear menú contextual (sin 'menubar') function ocultarVentana() {
const menu = new nw.Menu(); createTray();
menu.append(new nw.MenuItem({ type: 'checkbox', label: 'box1' })); win.hide();
menu.append(new nw.MenuItem({ win.setShowInTaskbar(false);
label: 'Iniciar', }
click: () => { if (typeof INICIAR === 'function') INICIAR(); }
})); function createTray() {
menu.append(new nw.MenuItem({ if (tray) return;
label: 'Detener',
click: () => { if (typeof DETENER === 'function') DETENER(); } const menu = new gui.Menu();
})); menu.append(new gui.MenuItem({
menu.append(new nw.MenuItem({ type: 'separator' }));
menu.append(new nw.MenuItem({
label: 'Mostrar ventana', label: 'Mostrar ventana',
click: () => { click: mostrarVentana
win.show();
win.restore();
win.focus();
win.setShowInTaskbar(true);
removeTray();
}
})); }));
menu.append(new nw.MenuItem({ menu.append(new gui.MenuItem({
label: 'Ocultar ventana', label: 'Ocultar ventana',
click: () => { click: ocultarVentana
win.hide();
win.setShowInTaskbar(false);
}
})); }));
menu.append(new nw.MenuItem({ type: 'separator' })); menu.append(new gui.MenuItem({ type: 'separator' }));
menu.append(new nw.MenuItem({ menu.append(new gui.MenuItem({ label: 'Iniciar', click: INICIAR }));
menu.append(new gui.MenuItem({ label: 'Detener', click: DETENER }));
menu.append(new gui.MenuItem({ type: 'separator' }));
menu.append(new gui.MenuItem({
label: 'Salir', label: 'Salir',
click: () => { click: () => {
if (!ConfigUI.canLeave()) return; if (!ConfigUI.canLeave()) return;
if (typeof EMA !== 'undefined' && typeof EMA.Salir === 'function') EMA.Salir(); if (typeof EMA.Salir === 'function') EMA.Salir();
nw.App.closeAllWindows(); gui.App.closeAllWindows();
nw.App.quit(); gui.App.quit();
} }
})); }));
// Crear tray icon // Entregar el menú al crear el indicador, también en Linux.
tray = new nw.Tray({ tray = new gui.Tray({
title: 'EmaLibre Carpincho', title: 'EmaLibre Carpincho',
icon: iconoApagado, icon: iconoApagado,
menu: menu
}); });
tray.menu = menu; tray.tooltip = 'Ema Libre Carpincho';
// NW.js abre el menú nativo con clic derecho; solo emite clic izquierdo.
// Click izquierdo: mostrar ventana tray.on('click', mostrarVentana);
tray.on('click', () => {
console.log('Tray icon clicked');
win.show();
win.restore();
win.focus();
win.setShowInTaskbar(true);
removeTray();
if (typeof MuestraRegistroActual === 'function') MuestraRegistroActual(false);
if (typeof Graficos === 'function') Graficos();
if (typeof ClimaActual === 'function') ClimaActual();
});
// Click derecho: abrir menú manualmente si el entorno no lo hace
tray.on('right-click', () => {
console.log('Tray icon right-clicked');
tray.menu.popup();
});
// Doble click: también mostrar
tray.on('double-click', () => {
win.show();
win.focus();
win.setShowInTaskbar(true);
removeTray();
});
} }
nw.Window.get().on('loaded', createTray); win.on('loaded', createTray);
win.on('minimize', () => {
ventanaMinimizada = true;
ocultarVentana();
});
// Window resizing is handled by responsive CSS; preserve manual zoom. // Window resizing is handled by responsive CSS; preserve manual zoom.
@ -171,7 +152,6 @@ if (_IS_NW) {
}; };
} }
// **********************Vairales Generales********************* // **********************Vairales Generales*********************
let tray = null;
var UltimoReg; var UltimoReg;
var Ant_UltimoReg; var Ant_UltimoReg;
var ModeloVisor; // Variables Generales - Registros recolectados var ModeloVisor; // Variables Generales - Registros recolectados
@ -204,7 +184,7 @@ function INICIAR () {
HABLA(0, 'Se inicio en modo Recolector', 'success'); HABLA(0, 'Se inicio en modo Recolector', 'success');
$('#signal').addClass('parpadea'); $('#signal').addClass('parpadea');
} }
if (_IS_NW) tray.icon = iconoEncendido; if (_IS_NW) { createTray(); tray.icon = iconoEncendido; }
$('td#barralateral').css('background-color', '#11881161'); $('td#barralateral').css('background-color', '#11881161');
$('#estado').html('En Linea'); $('#estado').html('En Linea');
$('#mensaje').html('Encendido'); $('#mensaje').html('Encendido');
@ -268,7 +248,7 @@ function killAllTimers() {
function DETENER () { function DETENER () {
clearInterval(intervaloBarra); clearInterval(intervaloBarra);
killAllTimers(); killAllTimers();
if (_IS_NW) tray.icon = iconoApagado; if (_IS_NW) { createTray(); tray.icon = iconoApagado; }
$('td#barralateral').css('background-color', '#ff000030'); $('td#barralateral').css('background-color', '#ff000030');
$('#estado').html('Fuera de Linea'); $('#estado').html('Fuera de Linea');
$('#img_estado').attr('src', iconoNeutro); $('#img_estado').attr('src', iconoNeutro);

View File

@ -5,9 +5,49 @@ const ConfigUI = (() => {
const values = () => Object.fromEntries(Object.keys(ConfiguracionModelo.fields).map(id => [id, document.getElementById(id)?.value ?? ''])); const values = () => Object.fromEntries(Object.keys(ConfiguracionModelo.fields).map(id => [id, document.getElementById(id)?.value ?? '']));
const snapshot = () => JSON.stringify(values()); const snapshot = () => JSON.stringify(values());
const dirty = () => !!form() && baseline !== snapshot(); const dirty = () => !!form() && baseline !== snapshot();
function message(text, error = false) { let notification;
function notify(text, error) {
if (notification) {
bootstrap.Toast.getInstance(notification)?.dispose();
notification.remove();
}
const toast = document.createElement('div');
toast.className = 'toast position-fixed top-0 end-0 m-3 border-0 shadow text-bg-' + (error ? 'danger' : 'success');
toast.style.zIndex = '1090';
toast.style.maxWidth = 'calc(100vw - 2rem)';
toast.setAttribute('role', error ? 'alert' : 'status');
toast.setAttribute('aria-live', error ? 'assertive' : 'polite');
toast.setAttribute('aria-atomic', 'true');
const row = document.createElement('div');
row.className = 'd-flex';
const body = document.createElement('div');
body.className = 'toast-body';
const title = document.createElement('strong');
title.className = 'd-block mb-1';
title.textContent = error ? 'No se pudo guardar' : 'Configuración guardada';
const detail = document.createElement('div');
detail.textContent = text;
body.append(title, detail);
const close = document.createElement('button');
close.type = 'button';
close.className = 'btn-close btn-close-white me-2 m-auto';
close.setAttribute('aria-label', 'Cerrar notificación');
close.setAttribute('data-bs-dismiss', 'toast');
row.append(body, close);
toast.append(row);
document.body.append(toast);
notification = toast;
toast.addEventListener('hidden.bs.toast', () => {
bootstrap.Toast.getInstance(toast)?.dispose();
toast.remove();
if (notification === toast) notification = null;
});
new bootstrap.Toast(toast, {autohide: !error, delay: 5000}).show();
}
function message(text, error = false, floating = false) {
const status = document.getElementById('config-status'); const status = document.getElementById('config-status');
if (status) { status.textContent = text; status.className = error ? 'text-danger' : 'text-white'; } if (status) { status.textContent = text; status.className = error ? 'text-danger' : 'text-white'; }
if (floating) notify(text, error);
} }
function canLeave() { function canLeave() {
if (saving) { message('Esperá a que termine el guardado.'); return false; } if (saving) { message('Esperá a que termine el guardado.'); return false; }
@ -72,7 +112,7 @@ const ConfigUI = (() => {
const data = values(); const data = values();
const invalid = ConfiguracionModelo.validate(data); const invalid = ConfiguracionModelo.validate(data);
errors(invalid); errors(invalid);
if (Object.keys(invalid).length) { message('Revisá los campos marcados.', true); return false; } if (Object.keys(invalid).length) { message('Revisá los campos marcados en el formulario.', true, true); return false; }
const draft = ConfiguracionModelo.build(original, data); const draft = ConfiguracionModelo.build(original, data);
saving = true; saving = true;
const controls = [...form().querySelectorAll('input, select, button')]; const controls = [...form().querySelectorAll('input, select, button')];
@ -81,11 +121,11 @@ const ConfigUI = (() => {
message('Guardando…'); message('Guardando…');
try { try {
await persist(draft); await persist(draft);
original = draft; baseline = snapshot(); message('Configuración guardada.'); original = draft; baseline = snapshot(); message('Configuración guardada.', false, true);
return draft; return draft;
} catch (error) { } catch (error) {
if (error.fields) errors(error.fields); if (error.fields) errors(error.fields);
message(error.message || 'No se pudo guardar. Tus cambios siguen en el formulario.', true); message(error.message || 'Tus cambios siguen en el formulario. Volvé a intentarlo.', true, true);
return false; return false;
} finally { } finally {
saving = false; saving = false;

View File

@ -21,7 +21,6 @@ const EstadoUI = (() => {
} }
target.append(box); target.append(box);
}; };
const configAction = {label: 'Revisar configuración', run() {document.getElementById('configuracion-tab')?.click();}};
if (data.error) {card('Conexión', 'Sin respuesta', data.error, 'danger', {label:'Reintentar', run: refresh}); return;} if (data.error) {card('Conexión', 'Sin respuesta', data.error, 'danger', {label:'Reintentar', run: refresh}); return;}
const visor = data.mode === 'Visor'; const visor = data.mode === 'Visor';
const running = data.state?.[0] === true; const running = data.state?.[0] === true;
@ -29,7 +28,7 @@ const EstadoUI = (() => {
const reason = data.state?.[1]; const reason = data.state?.[1];
card('Conexión', visor ? 'Fuente remota' : running ? 'Recolector activo' : 'Recolector detenido', card('Conexión', visor ? 'Fuente remota' : running ? 'Recolector activo' : 'Recolector detenido',
reason || (sensorFault ? 'La estación informa un problema de sensores. Revisá alimentación y señal.' : visor ? 'La fecha de lectura indica la vigencia del dato recibido.' : running ? 'El proceso está activo; revisá la última lectura para confirmar recepción.' : 'Iniciá el recolector desde Inicio o revisá la configuración.'), reason || (sensorFault ? 'La estación informa un problema de sensores. Revisá alimentación y señal.' : visor ? 'La fecha de lectura indica la vigencia del dato recibido.' : running ? 'El proceso está activo; revisá la última lectura para confirmar recepción.' : 'Iniciá el recolector desde Inicio o revisá la configuración.'),
reason || sensorFault ? 'warning' : running ? 'success' : 'secondary', reason || !running ? configAction : null); reason || sensorFault ? 'warning' : running ? 'success' : 'secondary');
const record = data.record; const record = data.record;
const age = Date.now() - date(record?.fecha, data.utc); const age = Date.now() - date(record?.fecha, data.utc);
const stale = Number.isFinite(age) && age > Math.max(120, Number(data.interval || 60) * 3) * 1000; const stale = Number.isFinite(age) && age > Math.max(120, Number(data.interval || 60) * 3) * 1000;
@ -40,7 +39,7 @@ const EstadoUI = (() => {
const failed = publications.find(p => p.estado !== true && !['Esperar', 'No enviado'].includes(p.estado)); const failed = publications.find(p => p.estado !== true && !['Esperar', 'No enviado'].includes(p.estado));
card('Última publicación', last ? `${last.red}: ${last.fecha}` : visor ? 'No corresponde al modo visor' : 'Sin publicaciones', card('Última publicación', last ? `${last.red}: ${last.fecha}` : visor ? 'No corresponde al modo visor' : 'Sin publicaciones',
failed ? `${failed.red}: ${failed.error || 'No se pudo publicar'}. Revisá la red y las credenciales.` : last ? (last.estado === true ? 'Envío confirmado por la red.' : 'La red indica que hay que esperar antes de enviar.') : 'Todavía no hay un envío confirmado.', failed ? `${failed.red}: ${failed.error || 'No se pudo publicar'}. Revisá la red y las credenciales.` : last ? (last.estado === true ? 'Envío confirmado por la red.' : 'La red indica que hay que esperar antes de enviar.') : 'Todavía no hay un envío confirmado.',
failed ? 'danger' : last?.estado === true ? 'success' : 'secondary', failed ? configAction : null); failed ? 'danger' : last?.estado === true ? 'success' : 'secondary');
} }
async function refresh() { async function refresh() {
clearTimeout(timer); clearTimeout(timer);

View File

@ -95,25 +95,27 @@
</div> </div>
<div class="col-12 col-md-6 col-lg-3 col-centered"> <div class="col-12 col-md-6 col-lg-3 col-centered">
<label class="control-label" for="Debug">Debug y Tamaño</label><br> <label class="control-label" for="Debug">Debug y Tamaño</label><br>
<select id='Debug' name="Debug" class="form-select"> <div class="input-group flex-nowrap">
<optgroup label="Normal"> <select id='Debug' name="Debug" class="form-select">
<option value="1">1</option> <optgroup label="Normal">
<option value="2">2</option> <option value="1">1</option>
<option value="3">3</option> <option value="2">2</option>
</optgroup> <option value="3">3</option>
<optgroup label="Alto"> </optgroup>
<option value="4">4</option> <optgroup label="Alto">
<option value="5">5</option> <option value="4">4</option>
<option value="6">6</option> <option value="5">5</option>
</optgroup> <option value="6">6</option>
<optgroup label="Muy Alto"> </optgroup>
<option value="7">7</option> <optgroup label="Muy Alto">
<option value="8">8</option> <option value="7">7</option>
<option value="9">9</option> <option value="8">8</option>
</optgroup> <option value="9">9</option>
</select> </optgroup>
<input type="text" class="form-control" id="debugMB" placeholder="MB Debug" maxlength="5" size="5"> </select>
<label class="control-label">MB</label> <input type="text" class="form-control" id="debugMB" aria-label="Tamaño máximo del debug en MB" placeholder="MB Debug" maxlength="5" size="5">
<span class="input-group-text">MB</span>
</div>
</div> </div>
<div class="col-12 col-md-6 col-lg-1 col-centered"> <div class="col-12 col-md-6 col-lg-1 col-centered">
<label class="control-label" for="utc">UTC</label><br> <label class="control-label" for="utc">UTC</label><br>

View File

@ -10,3 +10,23 @@
27/09/2026 23:21:09 - 0|CORE|✅ Modulo node-hid cargado correctamente. 27/09/2026 23:21:09 - 0|CORE|✅ Modulo node-hid cargado correctamente.
27/09/2026 23:21:09 - 0|CORE|⏳ Iniciando Ema Libre en Modo gráfico o Web. 27/09/2026 23:21:09 - 0|CORE|⏳ Iniciando Ema Libre en Modo gráfico o Web.
27/09/2026 23:23:30 - 0|CORE|🔚 SALIR: Se cerro el core de EMA LIBRE. Hasta luego!!!. 27/09/2026 23:23:30 - 0|CORE|🔚 SALIR: Se cerro el core de EMA LIBRE. Hasta luego!!!.
28/09/2026 21:04:14 - 0|CORE|🕵️. Buscando Modulo USB para el S.O - linux_x64_gui
28/09/2026 21:04:14 - 0|CORE|✅ Modulo node-hid cargado correctamente.
28/09/2026 21:04:14 - 0|CORE|⏳ Iniciando Ema Libre en Modo gráfico o Web.
28/09/2026 21:09:57 - 0|CORE|🔚 SALIR: Se cerro el core de EMA LIBRE. Hasta luego!!!.
28/09/2026 21:10:04 - 0|CORE|🕵️. Buscando Modulo USB para el S.O - linux_x64_gui
28/09/2026 21:10:04 - 0|CORE|✅ Modulo node-hid cargado correctamente.
28/09/2026 21:10:04 - 0|CORE|⏳ Iniciando Ema Libre en Modo gráfico o Web.
28/09/2026 21:11:11 - 0|CORE|🔚 SALIR: Se cerro el core de EMA LIBRE. Hasta luego!!!.
28/09/2026 21:12:28 - 0|CORE|🕵️. Buscando Modulo USB para el S.O - linux_x64_gui
28/09/2026 21:12:28 - 0|CORE|✅ Modulo node-hid cargado correctamente.
28/09/2026 21:12:28 - 0|CORE|⏳ Iniciando Ema Libre en Modo gráfico o Web.
28/09/2026 21:25:06 - 0|CORE|⚙️ RECONFIGURANDO: Se reconfigura el recolectorde EMA LIBRE.
28/09/2026 21:25:10 - 0|CORE|⚙️ RECONFIGURANDO: Se reconfigura el recolectorde EMA LIBRE.
28/09/2026 21:25:40 - 0|CORE|⚙️ RECONFIGURANDO: Se reconfigura el recolectorde EMA LIBRE.
28/09/2026 21:25:44 - 0|CORE|⚙️ RECONFIGURANDO: Se reconfigura el recolectorde EMA LIBRE.
28/09/2026 21:25:55 - 0|CORE|⚙️ RECONFIGURANDO: Se reconfigura el recolectorde EMA LIBRE.
28/09/2026 21:27:46 - 0|CORE|⚙️ RECONFIGURANDO: Se reconfigura el recolectorde EMA LIBRE.
28/09/2026 21:28:00 - 0|CORE|⚙️ RECONFIGURANDO: Se reconfigura el recolectorde EMA LIBRE.
28/09/2026 21:28:39 - 0|CORE|⚙️ RECONFIGURANDO: Se reconfigura el recolectorde EMA LIBRE.
28/09/2026 21:28:47 - 0|CORE|⚙️ RECONFIGURANDO: Se reconfigura el recolectorde EMA LIBRE.