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:
parent
e2c78fb63e
commit
c0ef4cb12a
39
app.py
39
app.py
|
@ -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"])
|
||||||
|
|
|
@ -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
63286
data/log.txt
File diff suppressed because it is too large
Load Diff
|
@ -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.');
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in New Issue