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 ---
|
||||
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"])
|
||||
|
|
|
@ -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 ===
|
||||
|
||||
// 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.');
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue