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 ---
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()
for ws in list(websocket_connections):
try:
if ws.connected:
ws.send(batch_message + "\n")
ws.send(message)
except Exception:
dead_connections.add(ws)
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")
def handle_websocket(ws):
try:
@ -92,7 +74,7 @@ def handle_websocket(ws):
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] ")
# Normalizar entrada a lista de mensajes
@ -116,10 +98,9 @@ def broadcast_message(message):
# Registrar en archivo (la clase Logger añade timestamp propio)
config_manager.append_log(raw_msg)
# Formatear para el WebSocket y añadir al buffer
formatted_msg_for_ws = f"{timestamp}{raw_msg}"
with buffer_lock:
broadcast_buffer.append(formatted_msg_for_ws)
# Enviar inmediatamente via WebSocket con timestamp
formatted_msg_for_ws = f"{timestamp}{raw_msg}\n"
_send_message_to_clients(formatted_msg_for_ws)
@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 ===
// Función para cambiar entre tabs
// Función para cambiar entre tabs (optimizada)
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
document.querySelectorAll('.tab-button').forEach(btn => {
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
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.add('hidden');
});
document.getElementById(`${tabName}-content`).classList.remove('hidden');
// Inicializar launcher si es la primera vez
if (tabName === 'launcher' && !window.launcherManager) {
window.launcherManager = new LauncherManager();
window.launcherManager.init();
const targetContent = document.getElementById(`${tabName}-content`);
if (targetContent) {
targetContent.classList.remove('hidden');
}
// Inicializar C# launcher si es la primera vez
// Inicializar managers solo si es necesario
if (tabName === 'launcher') {
if (!window.launcherManager) {
console.log('Initializing launcher manager...');
window.launcherManager = new LauncherManager();
window.launcherManager.init();
}
}
// Inicializar C# launcher solo si es necesario
if (tabName === 'csharp') {
if (!window.csharpLauncherManager) {
console.error('csharpLauncherManager not found! Make sure csharp_launcher.js is loaded.');
return;
}
if (!window.csharpLauncherManager.initialized) {
console.log('Initializing C# launcher manager...');
window.csharpLauncherManager.init();
}
}
// Inicializar Python launcher si es la primera vez
// Inicializar Python launcher solo si es necesario
if (tabName === 'python') {
if (typeof initPythonLauncher === 'function') {
initPythonLauncher();
if (!window.pythonLauncherInitialized) {
console.log('Initializing Python launcher...');
initPythonLauncher();
window.pythonLauncherInitialized = true;
}
} else {
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
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() {
// Comprobar si ya existe un socket y está abierto o conectándose
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
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 wsUrl = `${wsProtocol}//${window.location.host}/ws`;
@ -25,26 +26,27 @@ function initWebSocket() {
};
socket.onmessage = (event) => {
// console.log('Mensaje del servidor:', event.data); // Opcional: para depuración
addLogLine(event.data); // Llama a la función que ya tienes
// Procesar mensaje de forma más eficiente
if (event.data && event.data.trim()) {
addLogLine(event.data.trim());
}
};
socket.onerror = (error) => {
console.error('Error WebSocket:', error);
addLogLine('Error de conexión WebSocket.'); // Informar al usuario
addLogLine('Error de conexión WebSocket.');
};
socket.onclose = (event) => {
console.log('Conexión WebSocket cerrada:', event.code, event.reason);
// Opcional: intentar reconectar o informar al usuario
if (!event.wasClean) {
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() {
const group = currentGroup;
console.log('Loading configs for group:', group);
@ -55,34 +57,49 @@ async function loadConfigs() {
}
try {
// Cargar niveles 1 y 2
for (let level of [1, 2]) {
console.log(`Loading level ${level} config...`);
const response = await fetch(`/api/config/${level}?group=${group}`);
if (!response.ok) throw new Error(`Error loading level ${level} config`);
const data = await response.json();
console.log(`Level ${level} data:`, data);
await renderForm(`level${level}-form`, data);
// Cargar niveles 1 y 2 en paralelo
const level1Promise = fetch(`/api/config/1?group=${group}`);
const level2Promise = fetch(`/api/config/2?group=${group}`);
const workingDirPromise = fetch(`/api/working-directory/${group}`);
const [level1Response, level2Response, workingDirResponse] = await Promise.all([
level1Promise, level2Promise, workingDirPromise
]);
// 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
const workingDirResponse = await fetch(`/api/working-directory/${group}`);
const workingDirResult = await workingDirResponse.json();
// Procesar nivel 2
if (level2Response.ok) {
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) {
console.log('Loading level 3 config...');
const response = await fetch(`/api/config/3?group=${group}`);
if (!response.ok) throw new Error('Error loading level 3 config');
const data = await response.json();
console.log('Level 3 data:', data);
await renderForm('level3-form', data);
const level3Response = await fetch(`/api/config/3?group=${group}`);
if (level3Response.ok) {
const data3 = await level3Response.json();
console.log('Level 3 data:', data3);
await renderForm('level3-form', data3);
}
// Actualizar input del directorio de trabajo
document.getElementById('working-directory').value = workingDirResult.path;
const workingDirInput = document.getElementById('working-directory');
if (workingDirInput) {
workingDirInput.value = workingDirResult.path;
}
}
// Cargar scripts disponibles
await loadScripts(group);
// Cargar scripts disponibles (en paralelo con nivel 3)
loadScripts(group);
} catch (error) {
console.error('Error loading configs:', error);
@ -163,108 +180,128 @@ async function saveScriptDetails() {
}
}
// Load and display available scripts
// Load and display available scripts (optimized)
async function loadScripts(group) {
if (!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;
}
const response = await fetch(`/api/scripts/${group}`);
const scripts = await response.json();
const container = document.getElementById('scripts-list');
container.innerHTML = ''; // Limpiar contenedor antes de añadir nuevos elementos
scripts.forEach(script => {
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.innerHTML = `
<div>
<div class="font-bold text-lg mb-1">${script.name}</div>
<div class="flex items-center gap-2">
<span class="text-gray-600 text-sm">${script.description}</span>
${script.long_description ? `
<button class="toggle-long-desc-button text-blue-500 hover:text-blue-700 p-0.5 rounded" data-target-id="long-desc-${script.filename}" title="Mostrar/Ocultar detalles">
<svg class="w-4 h-4 chevron-down" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
<svg class="w-4 h-4 chevron-up hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path>
</svg>
</button>
` : ''}
</div>
<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
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 class="flex items-center gap-2 flex-shrink-0">
<div class="flex flex-col items-center">
<div class="flex gap-1">
<button data-filename="${script.filename}"
class="bg-green-500 hover:bg-green-600 text-white px-2 py-1 rounded text-sm execute-button"
title="Ejecutar script">
</button>
<button data-filename="${script.filename}"
class="bg-red-500 hover:bg-red-600 text-white px-2 py-1 rounded text-sm stop-button disabled:opacity-50 disabled:cursor-not-allowed"
disabled
title="Detener script">
</button>
</div>
<div class="text-xs text-gray-500 mt-1 truncate w-20 text-center" title="${script.filename}">${script.filename}</div>
</div>
<button data-group="${group}" data-filename="${script.filename}"
class="p-1 rounded text-gray-500 hover:bg-gray-200 hover:text-gray-700 edit-button" title="Editar Detalles">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
</button>
</div>
`;
container.appendChild(div);
// Añadir event listeners a los botones recién creados
const executeButton = div.querySelector('.execute-button');
executeButton.addEventListener('click', () => {
executeScript(script.filename);
});
const stopButton = div.querySelector('.stop-button');
stopButton.addEventListener('click', () => {
stopScript(script.filename);
});
const editButton = div.querySelector('.edit-button');
editButton.addEventListener('click', () => {
editScriptDetails(group, script.filename);
});
// Añadir event listener para el botón de descripción larga (si existe)
const toggleDescButton = div.querySelector('.toggle-long-desc-button');
if (toggleDescButton) {
toggleDescButton.addEventListener('click', (e) => {
const button = e.currentTarget;
const targetId = button.dataset.targetId;
const targetElement = document.getElementById(targetId);
if (targetElement) {
targetElement.classList.toggle('hidden');
button.querySelector('.chevron-down').classList.toggle('hidden');
button.querySelector('.chevron-up').classList.toggle('hidden');
}
});
try {
const response = await fetch(`/api/scripts/${group}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
});
const scripts = await response.json();
const container = document.getElementById('scripts-list');
if (!container) {
console.warn('Scripts list container not found');
return;
}
// Usar DocumentFragment para mejor rendimiento
const fragment = document.createDocumentFragment();
scripts.forEach(script => {
const div = document.createElement('div');
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>
<div class="font-bold text-lg mb-1">${script.name}</div>
<div class="flex items-center gap-2">
<span class="text-gray-600 text-sm">${script.description}</span>
${script.long_description ? `
<button class="toggle-long-desc-button text-blue-500 hover:text-blue-700 p-0.5 rounded" data-target-id="long-desc-${script.filename}" title="Mostrar/Ocultar detalles">
<svg class="w-4 h-4 chevron-down" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
<svg class="w-4 h-4 chevron-up hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path>
</svg>
</button>
` : ''}
</div>
<div id="long-desc-${script.filename}" class="long-description-content mt-2 border-t pt-2 hidden">
${longDescContent}
</div>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<div class="flex flex-col items-center">
<div class="flex gap-1">
<button data-filename="${script.filename}"
class="bg-green-500 hover:bg-green-600 text-white px-2 py-1 rounded text-sm execute-button"
title="Ejecutar script">
</button>
<button data-filename="${script.filename}"
class="bg-red-500 hover:bg-red-600 text-white px-2 py-1 rounded text-sm stop-button disabled:opacity-50 disabled:cursor-not-allowed"
disabled
title="Detener script">
</button>
</div>
<div class="text-xs text-gray-500 mt-1 truncate w-20 text-center" title="${script.filename}">${script.filename}</div>
</div>
<button data-group="${group}" data-filename="${script.filename}"
class="p-1 rounded text-gray-500 hover:bg-gray-200 hover:text-gray-700 edit-button" title="Editar Detalles">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
</button>
</div>
`;
// Añadir event listeners usando delegación de eventos más eficiente
const executeButton = div.querySelector('.execute-button');
executeButton.addEventListener('click', () => executeScript(script.filename));
const stopButton = div.querySelector('.stop-button');
stopButton.addEventListener('click', () => stopScript(script.filename));
const editButton = div.querySelector('.edit-button');
editButton.addEventListener('click', () => editScriptDetails(group, script.filename));
// Event listener para el botón de descripción larga
const toggleDescButton = div.querySelector('.toggle-long-desc-button');
if (toggleDescButton) {
toggleDescButton.addEventListener('click', (e) => {
const button = e.currentTarget;
const targetId = button.dataset.targetId;
const targetElement = document.getElementById(targetId);
if (targetElement) {
targetElement.classList.toggle('hidden');
button.querySelector('.chevron-down').classList.toggle('hidden');
button.querySelector('.chevron-up').classList.toggle('hidden');
}
});
}
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
@ -397,23 +434,32 @@ function handleScriptCompletion(message) {
}
}
// Form rendering functionality
// Form rendering functionality (optimized)
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);
if (!container) {
console.warn(`Container ${containerId} not found`);
return;
}
const level = containerId.replace('level', '').split('-')[0];
try {
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();
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) {
container.innerHTML = '<p class="text-gray-500">No hay esquema definido para este nivel.</p>';
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 buttonState = existingButton ? {
text: existingButton.innerText,
@ -421,7 +467,8 @@ async function renderForm(containerId, data) {
disabled: existingButton.disabled
} : null;
container.innerHTML = `
// Renderizar contenido usando template string más eficiente
const formHTML = `
<form id="config-form-${level}" class="space-y-4">
${generateFormFields(schema, data || {}, '', level)}
</form>
@ -433,12 +480,16 @@ async function renderForm(containerId, data) {
</div>
`;
container.innerHTML = formHTML;
// Restaurar el estado del botón si existía
if (buttonState) {
const newButton = document.getElementById(`save-config-${level}`);
newButton.innerText = buttonState.text;
newButton.className = buttonState.className;
newButton.disabled = buttonState.disabled;
if (newButton) {
newButton.innerText = buttonState.text;
newButton.className = buttonState.className;
newButton.disabled = buttonState.disabled;
}
}
} catch (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) {
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);
console.log('Group changed to:', currentGroup);
// Limpiar formularios existentes
['level1-form', 'level2-form', 'level3-form'].forEach(id => {
// Limpiar formularios existentes de forma más eficiente
const forms = ['level1-form', 'level2-form', 'level3-form'];
forms.forEach(id => {
const element = document.getElementById(id);
if (element) element.innerHTML = '';
});
// Actualizar la interfaz
updateGroupDescription();
await initWorkingDirectory();
await loadConfigs();
// Actualizar la interfaz en paralelo
const updatePromises = [
updateGroupDescription(),
initWorkingDirectory(),
loadConfigs()
];
await Promise.all(updatePromises);
// Cerrar sidebar en móviles
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) {
const logArea = document.getElementById('log-area');
if (!logArea) return;
// Message from WebSocket should already have timestamp.
// Trim any extra whitespace just in case.
const cleanMessage = String(message).trim();
if (cleanMessage) {
// Append the cleaned message + a newline for display separation.
logArea.innerHTML += cleanMessage + '\n';
logArea.scrollTop = logArea.scrollHeight; // Ensure scroll to bottom
// Usar textContent + newline es más eficiente que innerHTML concatenation
logArea.textContent += cleanMessage + '\n';
logArea.scrollTop = logArea.scrollHeight;
// Detectar finalización de scripts
handleScriptCompletion(cleanMessage);