Optimize tab switching, WebSocket initialization, and script loading for improved performance and user experience. Refactor form rendering and group change handling to reduce unnecessary operations and enhance code readability. Implement efficient DOM updates and error handling throughout the application.

This commit is contained in:
Miguel 2025-08-24 11:05:54 +02:00
parent e2c78fb63e
commit c0ef4cb12a
5 changed files with 247 additions and 63687 deletions

39
app.py
View File

@ -42,41 +42,23 @@ websocket_connections = set()
# --- Globals for Tray Icon --- # --- Globals for Tray Icon ---
tray_icon = None tray_icon = None
# --- Parámetros para envío por lotes de logs ---
BATCH_FLUSH_INTERVAL = 0.5 # segundos
broadcast_buffer = [] # Almacena líneas formateadas pendientes de envío
buffer_lock = threading.Lock() # Sincroniza acceso al buffer
# --- Parámetros para envío directo de logs (optimizado) ---
def _send_message_to_clients(message: str):
"""Envía un mensaje directamente a todas las conexiones WebSocket activas."""
if not websocket_connections:
return
def _broadcast_flush_loop():
"""Hilo que vacía el buffer de logs cada BATCH_FLUSH_INTERVAL segundos."""
while True:
time.sleep(BATCH_FLUSH_INTERVAL)
with buffer_lock:
if not broadcast_buffer:
continue
batch = "\n".join(broadcast_buffer)
broadcast_buffer.clear()
_send_batch_to_clients(batch)
def _send_batch_to_clients(batch_message: str):
"""Envía un bloque de texto a todas las conexiones WebSocket activas."""
dead_connections = set() dead_connections = set()
for ws in list(websocket_connections): for ws in list(websocket_connections):
try: try:
if ws.connected: if ws.connected:
ws.send(batch_message + "\n") ws.send(message)
except Exception: except Exception:
dead_connections.add(ws) dead_connections.add(ws)
websocket_connections.difference_update(dead_connections) websocket_connections.difference_update(dead_connections)
# Iniciar hilo de vaciado en segundo plano (ahora que las dependencias están definidas)
flusher_thread = threading.Thread(target=_broadcast_flush_loop, daemon=True)
flusher_thread.start()
@sock.route("/ws") @sock.route("/ws")
def handle_websocket(ws): def handle_websocket(ws):
try: try:
@ -92,7 +74,7 @@ def handle_websocket(ws):
def broadcast_message(message): def broadcast_message(message):
"""Acumula mensajes en un buffer y los envía por lotes cada 500 ms.""" """Envía mensajes directamente via WebSocket (optimizado)."""
timestamp = datetime.now().strftime("[%H:%M:%S] ") timestamp = datetime.now().strftime("[%H:%M:%S] ")
# Normalizar entrada a lista de mensajes # Normalizar entrada a lista de mensajes
@ -116,10 +98,9 @@ def broadcast_message(message):
# Registrar en archivo (la clase Logger añade timestamp propio) # Registrar en archivo (la clase Logger añade timestamp propio)
config_manager.append_log(raw_msg) config_manager.append_log(raw_msg)
# Formatear para el WebSocket y añadir al buffer # Enviar inmediatamente via WebSocket con timestamp
formatted_msg_for_ws = f"{timestamp}{raw_msg}" formatted_msg_for_ws = f"{timestamp}{raw_msg}\n"
with buffer_lock: _send_message_to_clients(formatted_msg_for_ws)
broadcast_buffer.append(formatted_msg_for_ws)
@app.route("/api/execute_script", methods=["POST"]) @app.route("/api/execute_script", methods=["POST"])

View File

@ -1,218 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script de debugging para entender por qué los índices de array simples como [#i] no funcionan
"""
import os
import sys
from lxml import etree
# Agregar el directorio del script al path
script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(script_dir)
from parsers.parse_scl import reconstruct_scl_from_tokens
from parsers.parser_utils import ns
def debug_array_parsing():
"""Función para debug del parsing de arrays"""
# Cargar el archivo XML problemático
xml_path = r"D:\Trabajo\VM\45 - HENKEL - VM Auto Changeover\ExportTia\PLC_TL27_Q1\ProgramBlocks_XML\FB HMI Interlock.xml"
print(f"Cargando XML: {xml_path}")
# Cargar y parsear el XML
with open(xml_path, "r", encoding="utf-8") as f:
content = f.read()
root = etree.fromstring(content)
# Buscar todas las instancias de i_Request
print("\n=== Buscando todas las instancias de i_Request ===")
# Buscar elementos Component con Name="i_Request"
components = root.xpath("//Component[@Name='i_Request']")
print(f"Encontrados {len(components)} componentes i_Request")
# Si no encontramos nada, buscar de manera más amplia
if len(components) == 0:
print("No se encontraron con XPath directo, buscando de manera recursiva...")
components = []
for elem in root.iter():
if elem.tag.endswith("Component") and elem.get("Name") == "i_Request":
components.append(elem)
print(
f"Encontrados {len(components)} componentes i_Request con búsqueda recursiva"
)
for i, comp in enumerate(components):
print(f"\n--- Componente {i+1} (UId={comp.get('UId')}) ---")
# Obtener todos los hijos
children = comp.xpath("./*")
print(f"Número de hijos: {len(children)}")
for j, child in enumerate(children):
tag = etree.QName(child.tag).localname
print(f" Hijo {j}: {tag}")
if tag == "Token":
print(f" Text: '{child.get('Text')}'")
elif tag == "Access":
print(f" Scope: '{child.get('Scope')}'")
# Buscar Symbol dentro del Access
symbols = child.xpath(".//Symbol")
if symbols:
symbol_components = symbols[0].xpath(".//Component")
for sc in symbol_components:
print(f" Component: '{sc.get('Name')}'")
# Verificar si este componente tiene patrón de array
has_array_pattern = False
bracket_start_idx = -1
bracket_end_idx = -1
# Buscar los tokens [ y ]
for idx, child in enumerate(children):
tag = etree.QName(child.tag).localname
if tag == "Token":
text = child.get("Text")
if text == "[" and bracket_start_idx == -1:
bracket_start_idx = idx
elif text == "]" and bracket_start_idx != -1:
bracket_end_idx = idx
break
if bracket_start_idx != -1 and bracket_end_idx != -1:
has_array_pattern = True
print(
f" Corchetes encontrados en índices: {bracket_start_idx} y {bracket_end_idx}"
)
print(f" Tiene patrón de array: {has_array_pattern}")
if has_array_pattern:
print(" >>> Este componente debería ser procesado como array <<<")
# Simular el procesamiento manual del Access del medio
for middle_idx in range(bracket_start_idx + 1, bracket_end_idx):
middle_child = children[middle_idx]
child_tag = etree.QName(middle_child.tag).localname
print(f" Procesando elemento medio {middle_idx}: {child_tag}")
if child_tag == "Access":
scope = middle_child.get("Scope")
print(f" Scope: {scope}")
if scope == "LocalVariable":
print(" >>> Es LocalVariable, procesando manualmente <<<")
# Debug: mostrar toda la estructura del Access
print(" Estructura completa del Access:")
def print_xml_structure(elem, indent=" "):
tag = etree.QName(elem.tag).localname
attrs = dict(elem.attrib)
print(f"{indent}{tag}: {attrs}")
for child in elem:
print_xml_structure(child, indent + " ")
print_xml_structure(middle_child)
# Buscar Symbol con diferentes métodos
symbol_elem_ns = middle_child.xpath(
"./st:Symbol", namespaces=ns
)
symbol_elem_no_ns = middle_child.xpath("./Symbol")
symbol_elem_recursive = []
for child in middle_child:
if etree.QName(child.tag).localname == "Symbol":
symbol_elem_recursive.append(child)
print(f" Symbol con namespace: {len(symbol_elem_ns)}")
print(f" Symbol sin namespace: {len(symbol_elem_no_ns)}")
print(
f" Symbol recursivo manual: {len(symbol_elem_recursive)}"
)
# Usar el método que funcione
symbol_elem = None
if symbol_elem_ns:
symbol_elem = symbol_elem_ns
print(" Usando Symbol con namespace")
elif symbol_elem_no_ns:
symbol_elem = symbol_elem_no_ns
print(" Usando Symbol sin namespace")
elif symbol_elem_recursive:
symbol_elem = symbol_elem_recursive
print(" Usando Symbol recursivo manual")
if symbol_elem:
print(
f" Procesando Symbol (total: {len(symbol_elem)})"
)
# Buscar componentes dentro del Symbol
components_inner_ns = symbol_elem[0].xpath(
"./st:Component", namespaces=ns
)
components_inner_no_ns = symbol_elem[0].xpath("./Component")
components_inner_manual = []
for child in symbol_elem[0]:
if etree.QName(child.tag).localname == "Component":
components_inner_manual.append(child)
print(
f" Componentes con namespace: {len(components_inner_ns)}"
)
print(
f" Componentes sin namespace: {len(components_inner_no_ns)}"
)
print(
f" Componentes manual: {len(components_inner_manual)}"
)
# Usar el método que funcione
components_inner = None
if components_inner_ns:
components_inner = components_inner_ns
print(" Usando componentes con namespace")
elif components_inner_no_ns:
components_inner = components_inner_no_ns
print(" Usando componentes sin namespace")
elif components_inner_manual:
components_inner = components_inner_manual
print(" Usando componentes manual")
if components_inner:
print(
f" Componentes internos encontrados: {len(components_inner)}"
)
result_parts = []
for k, comp_inner in enumerate(components_inner):
name = comp_inner.get("Name", "_ERR_COMP_")
print(f" Componente {k}: '{name}'")
if k == 0:
result_parts.append(f"#{name}")
print(f" -> Se convertirá en: #{name}")
else:
result_parts.append(f".{name}")
print(f" -> Se convertirá en: .{name}")
final_result = "".join(result_parts)
print(
f" >>> RESULTADO FINAL: '{final_result}' <<<"
)
else:
print(" ERROR: No se encontraron componentes")
else:
print(
" ERROR: No se encontró Symbol dentro del Access"
)
print("\n=== Fin del análisis ===")
if __name__ == "__main__":
debug_array_parsing()

63286
data/log.txt

File diff suppressed because it is too large Load Diff

View File

@ -1347,41 +1347,63 @@ class LauncherManager {
// === FUNCIONES GLOBALES === // === FUNCIONES GLOBALES ===
// Función para cambiar entre tabs // Función para cambiar entre tabs (optimizada)
function switchTab(tabName) { function switchTab(tabName) {
// Prevenir cambios innecesarios
const currentActiveTab = document.querySelector('.tab-button.active');
if (currentActiveTab && currentActiveTab.id === `${tabName}-tab`) {
return; // Ya está activo
}
// Cambiar tabs activos // Cambiar tabs activos
document.querySelectorAll('.tab-button').forEach(btn => { document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('active'); btn.classList.remove('active');
}); });
document.getElementById(`${tabName}-tab`).classList.add('active');
const targetTab = document.getElementById(`${tabName}-tab`);
if (targetTab) {
targetTab.classList.add('active');
}
// Cambiar contenido // Cambiar contenido
document.querySelectorAll('.tab-content').forEach(content => { document.querySelectorAll('.tab-content').forEach(content => {
content.classList.add('hidden'); content.classList.add('hidden');
}); });
document.getElementById(`${tabName}-content`).classList.remove('hidden');
// Inicializar launcher si es la primera vez const targetContent = document.getElementById(`${tabName}-content`);
if (tabName === 'launcher' && !window.launcherManager) { if (targetContent) {
targetContent.classList.remove('hidden');
}
// Inicializar managers solo si es necesario
if (tabName === 'launcher') {
if (!window.launcherManager) {
console.log('Initializing launcher manager...');
window.launcherManager = new LauncherManager(); window.launcherManager = new LauncherManager();
window.launcherManager.init(); window.launcherManager.init();
} }
}
// Inicializar C# launcher si es la primera vez // Inicializar C# launcher solo si es necesario
if (tabName === 'csharp') { if (tabName === 'csharp') {
if (!window.csharpLauncherManager) { if (!window.csharpLauncherManager) {
console.error('csharpLauncherManager not found! Make sure csharp_launcher.js is loaded.'); console.error('csharpLauncherManager not found! Make sure csharp_launcher.js is loaded.');
return; return;
} }
if (!window.csharpLauncherManager.initialized) { if (!window.csharpLauncherManager.initialized) {
console.log('Initializing C# launcher manager...');
window.csharpLauncherManager.init(); window.csharpLauncherManager.init();
} }
} }
// Inicializar Python launcher si es la primera vez // Inicializar Python launcher solo si es necesario
if (tabName === 'python') { if (tabName === 'python') {
if (typeof initPythonLauncher === 'function') { if (typeof initPythonLauncher === 'function') {
if (!window.pythonLauncherInitialized) {
console.log('Initializing Python launcher...');
initPythonLauncher(); initPythonLauncher();
window.pythonLauncherInitialized = true;
}
} else { } else {
console.error('initPythonLauncher function not found! Make sure python_launcher.js is loaded.'); console.error('initPythonLauncher function not found! Make sure python_launcher.js is loaded.');
} }

View File

@ -6,14 +6,15 @@ let runningConfigScripts = new Set();
// Initialize WebSocket connection // Initialize WebSocket connection
let socket = null; // Define socket en un alcance accesible (p.ej., globalmente o en el scope del módulo) let socket = null; // Define socket en un alcance accesible (p.ej., globalmente o en el scope del módulo)
// Initialize WebSocket connection (optimized)
function initWebSocket() { function initWebSocket() {
// Comprobar si ya existe un socket y está abierto o conectándose // Comprobar si ya existe un socket y está abierto o conectándose
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) { if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
console.log("WebSocket ya está abierto o conectándose."); console.log("WebSocket ya está abierto o conectándose.");
return; // No crear una nueva conexión return;
} }
// Determinar URL del WebSocket (ws:// o wss://) // Determinar URL del WebSocket
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${wsProtocol}//${window.location.host}/ws`; const wsUrl = `${wsProtocol}//${window.location.host}/ws`;
@ -25,26 +26,27 @@ function initWebSocket() {
}; };
socket.onmessage = (event) => { socket.onmessage = (event) => {
// console.log('Mensaje del servidor:', event.data); // Opcional: para depuración // Procesar mensaje de forma más eficiente
addLogLine(event.data); // Llama a la función que ya tienes if (event.data && event.data.trim()) {
addLogLine(event.data.trim());
}
}; };
socket.onerror = (error) => { socket.onerror = (error) => {
console.error('Error WebSocket:', error); console.error('Error WebSocket:', error);
addLogLine('Error de conexión WebSocket.'); // Informar al usuario addLogLine('Error de conexión WebSocket.');
}; };
socket.onclose = (event) => { socket.onclose = (event) => {
console.log('Conexión WebSocket cerrada:', event.code, event.reason); console.log('Conexión WebSocket cerrada:', event.code, event.reason);
// Opcional: intentar reconectar o informar al usuario
if (!event.wasClean) { if (!event.wasClean) {
addLogLine('Conexión WebSocket perdida. Intente recargar la página.'); addLogLine('Conexión WebSocket perdida. Intente recargar la página.');
} }
socket = null; // Restablecer la variable socket después de cerrar socket = null;
}; };
} }
// Load configurations for all levels // Load configurations for all levels (optimized)
async function loadConfigs() { async function loadConfigs() {
const group = currentGroup; const group = currentGroup;
console.log('Loading configs for group:', group); console.log('Loading configs for group:', group);
@ -55,34 +57,49 @@ async function loadConfigs() {
} }
try { try {
// Cargar niveles 1 y 2 // Cargar niveles 1 y 2 en paralelo
for (let level of [1, 2]) { const level1Promise = fetch(`/api/config/1?group=${group}`);
console.log(`Loading level ${level} config...`); const level2Promise = fetch(`/api/config/2?group=${group}`);
const response = await fetch(`/api/config/${level}?group=${group}`); const workingDirPromise = fetch(`/api/working-directory/${group}`);
if (!response.ok) throw new Error(`Error loading level ${level} config`);
const data = await response.json(); const [level1Response, level2Response, workingDirResponse] = await Promise.all([
console.log(`Level ${level} data:`, data); level1Promise, level2Promise, workingDirPromise
await renderForm(`level${level}-form`, data); ]);
// Procesar nivel 1
if (level1Response.ok) {
const data1 = await level1Response.json();
console.log('Level 1 data:', data1);
await renderForm('level1-form', data1);
} }
// Cargar nivel 3 solo si hay directorio de trabajo // Procesar nivel 2
const workingDirResponse = await fetch(`/api/working-directory/${group}`); if (level2Response.ok) {
const workingDirResult = await workingDirResponse.json(); const data2 = await level2Response.json();
console.log('Level 2 data:', data2);
await renderForm('level2-form', data2);
}
// Procesar directorio de trabajo y nivel 3
const workingDirResult = await workingDirResponse.json();
if (workingDirResult.status === 'success' && workingDirResult.path) { if (workingDirResult.status === 'success' && workingDirResult.path) {
console.log('Loading level 3 config...'); console.log('Loading level 3 config...');
const response = await fetch(`/api/config/3?group=${group}`); const level3Response = await fetch(`/api/config/3?group=${group}`);
if (!response.ok) throw new Error('Error loading level 3 config'); if (level3Response.ok) {
const data = await response.json(); const data3 = await level3Response.json();
console.log('Level 3 data:', data); console.log('Level 3 data:', data3);
await renderForm('level3-form', data); await renderForm('level3-form', data3);
// Actualizar input del directorio de trabajo
document.getElementById('working-directory').value = workingDirResult.path;
} }
// Cargar scripts disponibles // Actualizar input del directorio de trabajo
await loadScripts(group); const workingDirInput = document.getElementById('working-directory');
if (workingDirInput) {
workingDirInput.value = workingDirResult.path;
}
}
// Cargar scripts disponibles (en paralelo con nivel 3)
loadScripts(group);
} catch (error) { } catch (error) {
console.error('Error loading configs:', error); console.error('Error loading configs:', error);
@ -163,21 +180,43 @@ async function saveScriptDetails() {
} }
} }
// Load and display available scripts // Load and display available scripts (optimized)
async function loadScripts(group) { async function loadScripts(group) {
if (!group) { if (!group) {
console.warn("loadScripts called without group"); console.warn("loadScripts called without group");
document.getElementById('scripts-list').innerHTML = '<p class="text-gray-500">Selecciona un grupo para ver los scripts.</p>'; const container = document.getElementById('scripts-list');
if (container) {
container.innerHTML = '<p class="text-gray-500">Selecciona un grupo para ver los scripts.</p>';
}
return; return;
} }
try {
const response = await fetch(`/api/scripts/${group}`); const response = await fetch(`/api/scripts/${group}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const scripts = await response.json(); const scripts = await response.json();
const container = document.getElementById('scripts-list'); const container = document.getElementById('scripts-list');
container.innerHTML = ''; // Limpiar contenedor antes de añadir nuevos elementos if (!container) {
console.warn('Scripts list container not found');
return;
}
// Usar DocumentFragment para mejor rendimiento
const fragment = document.createDocumentFragment();
scripts.forEach(script => { scripts.forEach(script => {
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'script-item p-4 border rounded bg-white shadow-sm flex justify-between items-start gap-4'; div.className = 'script-item p-4 border rounded bg-white shadow-sm flex justify-between items-start gap-4';
// Crear contenido de forma más eficiente
const longDescContent = script.long_description ?
(typeof window.markdownit !== 'undefined' ?
window.markdownit().render(script.long_description) :
`<pre>${script.long_description}</pre>`) : '';
div.innerHTML = ` div.innerHTML = `
<div> <div>
<div class="font-bold text-lg mb-1">${script.name}</div> <div class="font-bold text-lg mb-1">${script.name}</div>
@ -195,16 +234,7 @@ async function loadScripts(group) {
` : ''} ` : ''}
</div> </div>
<div id="long-desc-${script.filename}" class="long-description-content mt-2 border-t pt-2 hidden"> <div id="long-desc-${script.filename}" class="long-description-content mt-2 border-t pt-2 hidden">
${script.long_description ? (() => { // Self-invoking function to handle markdown rendering ${longDescContent}
if (typeof window.markdownit === 'undefined') { // Check if markdownit is loaded
console.error("markdown-it library not loaded!");
return `<p class="text-red-500">Error: Librería Markdown no cargada.</p><pre>${script.long_description}</pre>`; // Fallback: show raw text
}
// Create instance and render
const md = window.markdownit();
const renderedHtml = md.render(script.long_description); // Renderizar
return renderedHtml;
})() : ''}
</div> </div>
</div> </div>
<div class="flex items-center gap-2 flex-shrink-0"> <div class="flex items-center gap-2 flex-shrink-0">
@ -232,25 +262,18 @@ async function loadScripts(group) {
</button> </button>
</div> </div>
`; `;
container.appendChild(div);
// Añadir event listeners a los botones recién creados // Añadir event listeners usando delegación de eventos más eficiente
const executeButton = div.querySelector('.execute-button'); const executeButton = div.querySelector('.execute-button');
executeButton.addEventListener('click', () => { executeButton.addEventListener('click', () => executeScript(script.filename));
executeScript(script.filename);
});
const stopButton = div.querySelector('.stop-button'); const stopButton = div.querySelector('.stop-button');
stopButton.addEventListener('click', () => { stopButton.addEventListener('click', () => stopScript(script.filename));
stopScript(script.filename);
});
const editButton = div.querySelector('.edit-button'); const editButton = div.querySelector('.edit-button');
editButton.addEventListener('click', () => { editButton.addEventListener('click', () => editScriptDetails(group, script.filename));
editScriptDetails(group, script.filename);
});
// Añadir event listener para el botón de descripción larga (si existe) // Event listener para el botón de descripción larga
const toggleDescButton = div.querySelector('.toggle-long-desc-button'); const toggleDescButton = div.querySelector('.toggle-long-desc-button');
if (toggleDescButton) { if (toggleDescButton) {
toggleDescButton.addEventListener('click', (e) => { toggleDescButton.addEventListener('click', (e) => {
@ -264,7 +287,21 @@ async function loadScripts(group) {
} }
}); });
} }
fragment.appendChild(div);
}); });
// Limpiar y actualizar contenedor en una sola operación
container.innerHTML = '';
container.appendChild(fragment);
} catch (error) {
console.error('Error loading scripts:', error);
const container = document.getElementById('scripts-list');
if (container) {
container.innerHTML = '<p class="text-red-500">Error cargando scripts.</p>';
}
}
} }
// Execute a script // Execute a script
@ -397,23 +434,32 @@ function handleScriptCompletion(message) {
} }
} }
// Form rendering functionality // Form rendering functionality (optimized)
async function renderForm(containerId, data) { async function renderForm(containerId, data) {
console.log(`Rendering form for ${containerId} with data:`, data); // Debug line console.log(`Rendering form for ${containerId} with data:`, data);
const container = document.getElementById(containerId); const container = document.getElementById(containerId);
if (!container) {
console.warn(`Container ${containerId} not found`);
return;
}
const level = containerId.replace('level', '').split('-')[0]; const level = containerId.replace('level', '').split('-')[0];
try { try {
const schemaResponse = await fetch(`/api/schema/${level}?group=${currentGroup}`); const schemaResponse = await fetch(`/api/schema/${level}?group=${currentGroup}`);
if (!schemaResponse.ok) {
throw new Error(`Schema request failed: ${schemaResponse.status}`);
}
const schema = await schemaResponse.json(); const schema = await schemaResponse.json();
console.log(`Schema for level ${level}:`, schema); // Debug line console.log(`Schema for level ${level}:`, schema);
if (!schema || !schema.properties || Object.keys(schema.properties).length === 0) { if (!schema || !schema.properties || Object.keys(schema.properties).length === 0) {
container.innerHTML = '<p class="text-gray-500">No hay esquema definido para este nivel.</p>'; container.innerHTML = '<p class="text-gray-500">No hay esquema definido para este nivel.</p>';
return; return;
} }
// Guardar el estado del botón si existe // Guardar el estado del botón si existe (solo si es necesario)
const existingButton = document.getElementById(`save-config-${level}`); const existingButton = document.getElementById(`save-config-${level}`);
const buttonState = existingButton ? { const buttonState = existingButton ? {
text: existingButton.innerText, text: existingButton.innerText,
@ -421,7 +467,8 @@ async function renderForm(containerId, data) {
disabled: existingButton.disabled disabled: existingButton.disabled
} : null; } : null;
container.innerHTML = ` // Renderizar contenido usando template string más eficiente
const formHTML = `
<form id="config-form-${level}" class="space-y-4"> <form id="config-form-${level}" class="space-y-4">
${generateFormFields(schema, data || {}, '', level)} ${generateFormFields(schema, data || {}, '', level)}
</form> </form>
@ -433,13 +480,17 @@ async function renderForm(containerId, data) {
</div> </div>
`; `;
container.innerHTML = formHTML;
// Restaurar el estado del botón si existía // Restaurar el estado del botón si existía
if (buttonState) { if (buttonState) {
const newButton = document.getElementById(`save-config-${level}`); const newButton = document.getElementById(`save-config-${level}`);
if (newButton) {
newButton.innerText = buttonState.text; newButton.innerText = buttonState.text;
newButton.className = buttonState.className; newButton.className = buttonState.className;
newButton.disabled = buttonState.disabled; newButton.disabled = buttonState.disabled;
} }
}
} catch (error) { } catch (error) {
console.error(`Error rendering form ${containerId}:`, error); console.error(`Error rendering form ${containerId}:`, error);
@ -1135,23 +1186,33 @@ async function initializeApp() {
} }
} }
// Separar la lógica del cambio de grupo en una función // Separar la lógica del cambio de grupo en una función (optimizada)
async function handleGroupChange(e) { async function handleGroupChange(e) {
try { try {
currentGroup = e.target.value; const newGroup = e.target.value;
if (newGroup === currentGroup) {
return; // No cambiar si es el mismo grupo
}
currentGroup = newGroup;
localStorage.setItem('selectedGroup', currentGroup); localStorage.setItem('selectedGroup', currentGroup);
console.log('Group changed to:', currentGroup); console.log('Group changed to:', currentGroup);
// Limpiar formularios existentes // Limpiar formularios existentes de forma más eficiente
['level1-form', 'level2-form', 'level3-form'].forEach(id => { const forms = ['level1-form', 'level2-form', 'level3-form'];
forms.forEach(id => {
const element = document.getElementById(id); const element = document.getElementById(id);
if (element) element.innerHTML = ''; if (element) element.innerHTML = '';
}); });
// Actualizar la interfaz // Actualizar la interfaz en paralelo
updateGroupDescription(); const updatePromises = [
await initWorkingDirectory(); updateGroupDescription(),
await loadConfigs(); initWorkingDirectory(),
loadConfigs()
];
await Promise.all(updatePromises);
// Cerrar sidebar en móviles // Cerrar sidebar en móviles
if (window.innerWidth < 768) { if (window.innerWidth < 768) {
@ -1177,18 +1238,18 @@ function getTimestamp() {
}); });
} }
// Función para agregar línea al log con timestamp // Función para agregar línea al log con timestamp (optimizada)
function addLogLine(message) { function addLogLine(message) {
const logArea = document.getElementById('log-area'); const logArea = document.getElementById('log-area');
if (!logArea) return;
// Message from WebSocket should already have timestamp. // Message from WebSocket should already have timestamp.
// Trim any extra whitespace just in case.
const cleanMessage = String(message).trim(); const cleanMessage = String(message).trim();
if (cleanMessage) { if (cleanMessage) {
// Append the cleaned message + a newline for display separation. // Usar textContent + newline es más eficiente que innerHTML concatenation
logArea.innerHTML += cleanMessage + '\n'; logArea.textContent += cleanMessage + '\n';
logArea.scrollTop = logArea.scrollHeight; // Ensure scroll to bottom logArea.scrollTop = logArea.scrollHeight;
// Detectar finalización de scripts // Detectar finalización de scripts
handleScriptCompletion(cleanMessage); handleScriptCompletion(cleanMessage);