let socket; let currentGroup; // Initialize WebSocket connection function initWebSocket() { socket = new WebSocket(`ws://${location.host}/ws`); socket.onmessage = function(event) { addLogLine(event.data); }; socket.onclose = function() { console.log('WebSocket cerrado, intentando reconexión...'); setTimeout(initWebSocket, 1000); }; socket.onerror = function(error) { console.error('Error en WebSocket:', error); }; } // Load configurations for all levels async function loadConfigs() { const group = currentGroup; console.log('Loading configs for group:', group); if (!group) { console.error('No group selected'); return; } 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 nivel 3 solo si hay directorio de trabajo const workingDirResponse = await fetch(`/api/working-directory/${group}`); 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); // Actualizar input del directorio de trabajo document.getElementById('working-directory').value = workingDirResult.path; } // Cargar scripts disponibles await loadScripts(group); } catch (error) { console.error('Error loading configs:', error); } } // Load and display available scripts async function loadScripts(group) { const response = await fetch(`/api/scripts/${group}`); const scripts = await response.json(); const container = document.getElementById('scripts-list'); container.innerHTML = scripts.map(script => `
${script.name}
${script.description}
`).join(''); } // Execute a script async function executeScript(scriptName) { addLogLine(`\nEjecutando script: ${scriptName}...\n`); const response = await fetch('/api/execute_script', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ group: currentGroup, script: scriptName }) }); const result = await response.json(); if (result.error) { addLogLine(`\nError: ${result.error}\n`); } } // Form rendering functionality async function renderForm(containerId, data) { console.log(`Rendering form for ${containerId} with data:`, data); // Debug line const container = document.getElementById(containerId); const level = containerId.replace('level', '').split('-')[0]; try { const schemaResponse = await fetch(`/api/schema/${level}?group=${currentGroup}`); const schema = await schemaResponse.json(); console.log(`Schema for level ${level}:`, schema); // Debug line if (!schema || !schema.properties || Object.keys(schema.properties).length === 0) { container.innerHTML = '

No hay esquema definido para este nivel.

'; return; } // Guardar el estado del botón si existe const existingButton = document.getElementById(`save-config-${level}`); const buttonState = existingButton ? { text: existingButton.innerText, className: existingButton.className, disabled: existingButton.disabled } : null; container.innerHTML = `
${generateFormFields(schema, data || {}, '', level)}
`; // 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; } } catch (error) { console.error(`Error rendering form ${containerId}:`, error); container.innerHTML = '

Error cargando el esquema.

'; } } function generateFormFields(schema, data, prefix, level) { console.log('Generating fields with data:', { schema, data, prefix, level }); // Debug line let html = ''; if (!schema.properties) { console.warn('Schema has no properties'); return html; } for (const [key, def] of Object.entries(schema.properties)) { const fullKey = prefix ? `${prefix}.${key}` : key; const value = getValue(data, fullKey); console.log(`Field ${fullKey}:`, { definition: def, value: value }); // Debug line html += `
`; if (def.type === 'object') { html += `
${generateFormFields(def, data, fullKey, level)}
`; } else { html += generateInputField(def, fullKey, value, level); } if (def.description) { html += `

${def.description}

`; } html += '
'; } return html; } function getValue(data, path) { console.log('Getting value for path:', { path, data }); // Debug line if (!data || !path) return undefined; const value = path.split('.').reduce((obj, key) => obj?.[key], data); console.log('Found value:', value); // Debug line return value; } // Modificar la función generateInputField para quitar el onchange function generateInputField(def, key, value, level) { const baseClasses = "w-full p-2 border rounded bg-green-50"; switch (def.type) { case 'string': if (def.format === 'directory') { return `
`; } if (def.enum) { return ``; } return ``; case 'number': return ``; case 'boolean': return `
`; default: return ``; } } // Agregar nueva función para manejar la búsqueda de directorio async function browseFieldDirectory(button) { const input = button.parentElement.querySelector('input'); const currentPath = input.value; try { const response = await fetch(`/api/browse-directories?current_path=${encodeURIComponent(currentPath)}`); const result = await response.json(); if (result.status === 'success') { input.value = result.path; // Disparar un evento change para actualizar el valor internamente const event = new Event('change', { bubbles: true }); input.dispatchEvent(event); } } catch (error) { console.error('Error browsing directory:', error); alert('Error al buscar el directorio'); } } async function modifySchema(level) { try { console.log('Loading schema for level:', level); // Debug line const response = await fetch(`/api/schema/${level}?group=${currentGroup}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const schema = await response.json(); console.log('Loaded schema:', schema); // Debug line // Show schema editor modal const modal = document.getElementById('schema-editor'); if (!modal) { throw new Error('Schema editor modal not found'); } modal.classList.remove('hidden'); // Inicializar el esquema si está vacío const finalSchema = Object.keys(schema).length === 0 ? { type: 'object', properties: {} } : schema; // Inicializar editores const jsonEditor = document.getElementById('json-editor'); const visualEditor = document.getElementById('visual-editor'); const schemaLevel = document.getElementById('schema-level'); if (!jsonEditor || !visualEditor || !schemaLevel) { throw new Error('Required editor elements not found'); } jsonEditor.value = JSON.stringify(finalSchema, null, 2); visualEditor.innerHTML = '
' + ''; schemaLevel.value = level; // Renderizar editor visual renderVisualEditor(finalSchema); // Activar pestaña visual por defecto switchEditorMode('visual'); } catch (error) { console.error('Error loading schema:', error); alert('Error cargando el esquema: ' + error.message); } } function switchEditorMode(mode) { const visualEditor = document.getElementById('visual-editor'); const jsonEditor = document.getElementById('json-editor'); const visualTab = document.getElementById('visual-tab'); const jsonTab = document.getElementById('json-tab'); if (mode === 'visual') { visualEditor.classList.remove('hidden'); jsonEditor.classList.add('hidden'); visualTab.classList.add('border-blue-500'); jsonTab.classList.remove('border-blue-500'); // Actualizar el editor visual desde JSON try { const schema = JSON.parse(jsonEditor.value); renderVisualEditor(schema); } catch (e) { console.error('Error parsing JSON:', e); } } else { visualEditor.classList.add('hidden'); jsonEditor.classList.remove('hidden'); visualTab.classList.remove('border-blue-500'); jsonTab.classList.add('border-blue-500'); // Actualizar el JSON desde el editor visual try { const schema = updateVisualSchema(); jsonEditor.value = JSON.stringify(schema, null, 2); } catch (e) { console.error('Error updating JSON:', e); } } } function renderVisualEditor(schema) { const container = document.getElementById('schema-fields'); container.innerHTML = ''; Object.entries(schema.properties || {}).forEach(([key, field]) => { container.appendChild(createFieldEditor(key, field)); }); } function createFieldEditor(key, field) { const div = document.createElement('div'); div.className = 'mb-6 p-4 border rounded schema-field'; div.innerHTML = `
${field.enum ? `
` : ''} `; return div; } function updateFieldType(select) { const fieldContainer = select.closest('.schema-field'); const enumContainer = fieldContainer.querySelector('.enum-container'); if (select.value === 'enum') { if (!enumContainer) { const div = document.createElement('div'); div.className = 'enum-container mt-4'; div.innerHTML = ` `; fieldContainer.appendChild(div); } } else if (enumContainer) { enumContainer.remove(); } updateVisualSchema(); } function removeField(button) { const fieldContainer = button.closest('.schema-field'); fieldContainer.remove(); updateVisualSchema(); } function createEnumEditor(enumValues) { return `
`; } function addSchemaField() { const container = document.getElementById('schema-fields'); const newField = createFieldEditor(`campo_${Date.now()}`, { type: 'string', title: 'Nuevo Campo', description: '' }); container.appendChild(newField); } // Funciones de actualización del esquema visual function updateVisualSchema() { try { const fields = document.getElementById('schema-fields').children; const schema = { type: 'object', properties: {} }; Array.from(fields).forEach(field => { const inputs = field.getElementsByTagName('input'); const select = field.getElementsByTagName('select')[0]; const key = inputs[0].value; if (select.value === 'directory') { schema.properties[key] = { type: 'string', format: 'directory', title: inputs[1].value, description: inputs[2].value }; } else if (select.value === 'enum') { schema.properties[key] = { type: 'string', title: inputs[1].value, description: inputs[2].value, enum: field.querySelector('textarea').value.split('\n').filter(v => v.trim()) }; } else { schema.properties[key] = { type: select.value, title: inputs[1].value, description: inputs[2].value }; } }); const jsonEditor = document.getElementById('json-editor'); if (jsonEditor) { jsonEditor.value = JSON.stringify(schema, null, 2); } return schema; } catch (error) { console.error('Error updating schema:', error); return null; } } async function saveSchema() { try { const level = document.getElementById('schema-level').value; let schema; // Obtener el esquema según el modo activo const visualEditor = document.getElementById('visual-editor'); const jsonEditor = document.getElementById('json-editor'); if (!visualEditor.classList.contains('hidden')) { schema = updateVisualSchema(); } else { schema = JSON.parse(jsonEditor.value); } console.log('Saving schema:', schema); // Debug line const response = await fetch(`/api/schema/${level}?group=${currentGroup}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(schema) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } // Recargar el formulario const configResponse = await fetch(`/api/config/${level}?group=${currentGroup}`); const data = await configResponse.json(); await renderForm(`level${level}-form`, data); // Cerrar modal document.getElementById('schema-editor').classList.add('hidden'); } catch (e) { console.error('Error saving schema:', e); alert('Error guardando esquema: ' + e.message); } } async function setWorkingDirectory() { if (!currentGroup) { alert('Por favor, seleccione un grupo de scripts primero'); return; } const path = document.getElementById('working-directory').value; await updateWorkingDirectory(path); } async function initWorkingDirectory() { if (!currentGroup) return; const response = await fetch(`/api/working-directory/${currentGroup}`); const result = await response.json(); if (result.status === 'success' && result.path) { await updateWorkingDirectory(result.path); } } async function browseDirectory() { console.log('Current group when browsing:', currentGroup); // Debug line if (!currentGroup) { alert('Por favor, seleccione un grupo de scripts primero'); return; } const currentPath = document.getElementById('working-directory').value; const response = await fetch(`/api/browse-directories?current_path=${encodeURIComponent(currentPath)}`); const result = await response.json(); if (result.status === 'success') { await updateWorkingDirectory(result.path); } } // Nueva función auxiliar para actualizar el directorio de trabajo async function updateWorkingDirectory(path) { console.log('Updating working directory:', { path, group: currentGroup }); // Debug line const response = await fetch('/api/working-directory', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: path, group: currentGroup }) }); const result = await response.json(); console.log('Update result:', result); // Debug line if (result.status === 'success') { // Actualizar input document.getElementById('working-directory').value = path; // Recargar configuración de nivel 3 const configResponse = await fetch(`/api/config/3?group=${currentGroup}`); const data = await configResponse.json(); await renderForm('level3-form', data); } else { alert('Error: ' + (result.message || 'No se pudo actualizar el directorio de trabajo')); } } // Función para alternar visibilidad de una sección function toggleConfig(sectionId) { const content = document.getElementById(sectionId); const button = document.querySelector(`[onclick="toggleConfig('${sectionId}')"]`); if (content.classList.contains('hidden')) { content.classList.remove('hidden'); button.innerText = 'Ocultar Configuración'; // Recargar la configuración al mostrar const level = sectionId.replace('level', '').replace('-content', ''); const formId = `level${level}-form`; console.log(`Reloading config for level ${level}`); // Debug line fetch(`/api/config/${level}?group=${currentGroup}`) .then(response => response.json()) .then(data => renderForm(formId, data)) .catch(error => console.error('Error reloading config:', error)); } else { content.classList.add('hidden'); button.innerText = 'Mostrar Configuración'; } } async function clearLogs() { const response = await fetch('/api/logs', { method: 'DELETE' }); const result = await response.json(); if (result.status === 'success') { document.getElementById('log-area').innerHTML = ''; } } async function loadStoredLogs() { const response = await fetch('/api/logs'); const result = await response.json(); const logArea = document.getElementById('log-area'); logArea.innerHTML = result.logs; logArea.scrollTop = logArea.scrollHeight; } // Initialize on page load async function initializeApp() { try { // Inicializar WebSocket initWebSocket(); await loadStoredLogs(); // Configurar grupo actual const selectElement = document.getElementById('script-group'); currentGroup = localStorage.getItem('selectedGroup') || selectElement.value; // Actualizar el select con el valor guardado if (currentGroup) { selectElement.value = currentGroup; } // Limpiar evento anterior si existe selectElement.removeEventListener('change', handleGroupChange); // Agregar el nuevo manejador de eventos selectElement.addEventListener('change', handleGroupChange); // Cargar datos iniciales updateGroupDescription(); await initWorkingDirectory(); await loadConfigs(); } catch (error) { console.error('Error during initialization:', error); } } // Separar la lógica del cambio de grupo en una función async function handleGroupChange(e) { try { currentGroup = e.target.value; localStorage.setItem('selectedGroup', currentGroup); console.log('Group changed to:', currentGroup); // Limpiar formularios existentes ['level1-form', 'level2-form', 'level3-form'].forEach(id => { const element = document.getElementById(id); if (element) element.innerHTML = ''; }); // Actualizar la interfaz updateGroupDescription(); await initWorkingDirectory(); await loadConfigs(); // Cerrar sidebar en móviles if (window.innerWidth < 768) { toggleSidebar(); } } catch (error) { console.error('Error in handleGroupChange:', error); } } // Modificar la inicialización para usar la nueva función async document.addEventListener('DOMContentLoaded', () => { initializeApp().catch(console.error); }); // Función auxiliar para obtener timestamp formateado function getTimestamp() { const now = new Date(); return now.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); } // Función para agregar línea al log con timestamp function addLogLine(message) { const logArea = document.getElementById('log-area'); const timestamp = getTimestamp(); // Filtrar líneas vacías y aplicar timestamp solo a líneas con contenido const lines = message.split('\n') .filter(line => line.trim()) // Eliminar líneas vacías .map(line => `[${timestamp}] ${line}`) .join('\n'); if (lines) { logArea.innerHTML += lines + '\n'; logArea.scrollTop = logArea.scrollHeight; } } function updateGroupDescription() { const select = document.getElementById('script-group'); const option = select.options[select.selectedIndex]; const description = option.getAttribute('data-description'); document.getElementById('group-description').textContent = description; } function toggleSidebar() { const sidebar = document.querySelector('.sidebar'); const overlay = document.querySelector('.overlay'); const schemaEditor = document.getElementById('schema-editor'); // No cerrar sidebar si el modal está abierto if (!schemaEditor.classList.contains('hidden')) { return; } sidebar.classList.toggle('open'); overlay.classList.toggle('show'); } async function editGroupDescription() { if (!currentGroup) { alert('Por favor, seleccione un grupo de scripts primero'); return; } try { const response = await fetch(`/api/group-description/${currentGroup}`); if (!response.ok) throw new Error('Error cargando descripción del grupo'); const description = await response.json(); // Show schema editor modal with description data const modal = document.getElementById('schema-editor'); const modalTitle = modal.querySelector('h3'); const visualEditor = document.getElementById('visual-editor'); const jsonEditor = document.getElementById('json-editor'); const tabs = document.getElementById('editor-tabs'); // Configurar modal para edición de descripción modalTitle.textContent = 'Editar Descripción del Grupo'; tabs.classList.add('hidden'); // Crear el formulario en el visualEditor visualEditor.innerHTML = `
`; visualEditor.classList.remove('hidden'); jsonEditor.classList.add('hidden'); modal.classList.remove('hidden'); // Cambiar comportamiento de todos los botones de guardar const saveButtons = modal.querySelectorAll('button[onclick="saveSchema()"]'); saveButtons.forEach(btn => { btn.onclick = async () => { try { const form = document.getElementById('group-description-form'); const formData = new FormData(form); const updatedDescription = { name: formData.get('name') || '', description: formData.get('description') || '', version: formData.get('version') || '1.0', author: formData.get('author') || '' }; const saveResponse = await fetch(`/api/group-description/${currentGroup}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updatedDescription) }); if (!saveResponse.ok) throw new Error('Error guardando descripción'); // Restaurar modal a su estado original modalTitle.textContent = 'Editor de Esquema'; tabs.classList.remove('hidden'); saveButtons.forEach(btn => btn.onclick = saveSchema); modal.classList.add('hidden'); // Recargar la página para actualizar la descripción location.reload(); } catch (e) { alert('Error guardando descripción: ' + e.message); } }; }); } catch (e) { alert('Error: ' + e.message); } } // Agregar función para recolectar datos del formulario function collectFormData(level) { const formContainer = document.getElementById(`level${level}-form`); const data = {}; formContainer.querySelectorAll('input, select').forEach(input => { const key = input.getAttribute('data-key'); if (!key) return; let value; if (input.type === 'checkbox') { value = input.checked; } else if (input.type === 'number') { value = Number(input.value); } else { value = input.value; } // Manejar claves anidadas (por ejemplo: "parent.child") const keys = key.split('.'); let current = data; for (let i = 0; i < keys.length - 1; i++) { current[keys[i]] = current[keys[i]] || {}; current = current[keys[i]]; } current[keys[keys.length - 1]] = value; }); return data; } // Agregar función para guardar configuración async function saveConfig(level) { const saveButton = document.getElementById(`save-config-${level}`); if (!saveButton || saveButton.disabled) return; // Evitar múltiples envíos const originalText = saveButton.innerText; const originalClasses = 'bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded transition-colors duration-300'; try { saveButton.disabled = true; saveButton.className = 'bg-yellow-500 text-white px-4 py-2 rounded cursor-wait transition-colors duration-300'; saveButton.innerText = 'Guardando...'; const formData = collectFormData(level); const response = await fetch(`/api/config/${level}?group=${currentGroup}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const result = await response.json(); if (result.status === 'success') { saveButton.className = 'bg-green-500 text-white px-4 py-2 rounded transition-colors duration-300'; saveButton.innerText = '¡Guardado con Éxito!'; setTimeout(() => { if (saveButton) { saveButton.className = originalClasses; saveButton.innerText = originalText; saveButton.disabled = false; } }, 2000); } else { throw new Error(result.message || 'Error desconocido'); } } catch (error) { console.error('Error saving config:', error); saveButton.className = 'bg-red-500 text-white px-4 py-2 rounded transition-colors duration-300'; saveButton.innerText = 'Error al Guardar'; setTimeout(() => { if (saveButton) { saveButton.className = originalClasses; saveButton.innerText = originalText; saveButton.disabled = false; } }, 2000); } }