let currentGroup; // Initialize WebSocket connection let socket = null; // Define socket en un alcance accesible (p.ej., globalmente o en el scope del módulo) 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 } // Determinar URL del WebSocket (ws:// o wss://) const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${wsProtocol}//${window.location.host}/ws`; console.log("Inicializando conexión WebSocket a:", wsUrl); socket = new WebSocket(wsUrl); socket.onopen = () => { console.log('Conexión WebSocket establecida'); }; 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 }; socket.onerror = (error) => { console.error('Error WebSocket:', error); addLogLine('Error de conexión WebSocket.'); // Informar al usuario }; 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 }; } // 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); } } // --- Funciones para Editar Detalles del Script --- async function editScriptDetails(group, scriptFilename) { console.log(`[1] editScriptDetails called for: group=${group}, script=${scriptFilename}`); // Log inicial try { console.log('[2] Fetching script details...'); // Log antes del fetch const response = await fetch(`/api/script-details/${group}/${scriptFilename}`); console.log('[3] Fetch response received:', response); // Log después del fetch if (!response.ok) { console.error(`[!] Fetch error: ${response.status} ${response.statusText}`); // Log si la respuesta no es OK throw new Error(`Error fetching script details: ${response.statusText}`); } console.log('[4] Parsing JSON response...'); // Log antes de parsear JSON const details = await response.json(); console.log('[5] Script details received:', details); // Log con los detalles // Poblar el modal document.getElementById('edit-script-group').value = group; document.getElementById('edit-script-filename').value = scriptFilename; document.getElementById('edit-script-filename-display').textContent = scriptFilename; // Mostrar nombre de archivo document.getElementById('edit-script-display-name').value = details.display_name || ''; document.getElementById('edit-script-short-description').value = details.short_description || ''; // Poblar descripción corta document.getElementById('edit-script-long-description').value = details.long_description || ''; document.getElementById('edit-script-hidden').checked = details.hidden || false; console.log('[6] Populated modal fields.'); // Log después de poblar // Mostrar el modal document.getElementById('script-editor-modal').classList.remove('hidden'); console.log('[7] Modal should be visible now.'); // Log final } catch (error) { console.error('[!] Error in editScriptDetails:', error); // Log en el catch alert(`Error al cargar detalles del script: ${error.message}`); } } function closeScriptEditorModal() { document.getElementById('script-editor-modal').classList.add('hidden'); // Limpiar campos si es necesario (opcional) // document.getElementById('edit-script-display-name').value = ''; // document.getElementById('edit-script-long-description').value = ''; // document.getElementById('edit-script-hidden').checked = false; } async function saveScriptDetails() { const group = document.getElementById('edit-script-group').value; const scriptFilename = document.getElementById('edit-script-filename').value; const updatedDetails = { display_name: document.getElementById('edit-script-display-name').value, short_description: document.getElementById('edit-script-short-description').value, // Recoger descripción corta long_description: document.getElementById('edit-script-long-description').value, hidden: document.getElementById('edit-script-hidden').checked }; try { const response = await fetch(`/api/script-details/${group}/${scriptFilename}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updatedDetails) }); const result = await response.json(); if (!response.ok || result.status !== 'success') { throw new Error(result.message || `Error guardando detalles: ${response.statusText}`); } closeScriptEditorModal(); await loadScripts(currentGroup); // Recargar la lista de scripts showToast('Detalles del script guardados con éxito.'); } catch (error) { console.error('Error saving script details:', error); alert(`Error al guardar detalles del script: ${error.message}`); } } // Load and display available scripts async function loadScripts(group) { if (!group) { console.warn("loadScripts called without group"); document.getElementById('scripts-list').innerHTML = '

Selecciona un grupo para ver los scripts.

'; 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 = `
${script.name}
${script.description} ${script.long_description ? ` ` : ''}
${script.filename}
`; 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 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'); } }); } }); } // Execute a script async function executeScript(scriptName) { // REMOVE this line - let the backend log the start via WebSocket // addLogLine(`\nEjecutando script: ${scriptName}...\n`); try { const response = await fetch('/api/execute_script', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ group: currentGroup, script: scriptName }) // scriptName aquí es el filename real }); // Check for HTTP errors during the *request* itself if (!response.ok) { const errorText = await response.text(); console.error(`Error initiating script execution request: ${response.status} ${response.statusText}`, errorText); // Log only the request error, not script execution errors which come via WebSocket addLogLine(`\nError al iniciar la petición del script: ${response.status} ${errorText}\n`); return; // Stop if the request failed } // REMOVE logging the result/error here - let the backend log via WebSocket // const result = await response.json(); // Still potentially need to read body if not done elsewhere // if (result.error) { // addLogLine(`\nError: ${result.error}\n`); // } // Script output and final status/errors will arrive via WebSocket messages // handled by socket.onmessage -> addLogLine } catch (error) { console.error('Error in executeScript fetch:', error); addLogLine(`\nError de red o JavaScript al intentar ejecutar el script: ${error.message}\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 ` `; // <-- Añadir esta comilla invertida 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; const fieldType = select.value; // string, directory, number, boolean, enum const title = inputs[1].value; const description = inputs[2].value; const defaultValueInput = inputs[3]; // El nuevo input de valor por defecto const defaultValueString = defaultValueInput.value; let propertyDefinition = { type: fieldType === 'directory' || fieldType === 'enum' ? 'string' : fieldType, // El tipo base title: title, description: description }; // Añadir formato específico si es directorio if (select.value === 'directory') { propertyDefinition.format = 'directory'; } // Añadir enum si es de tipo enum if (select.value === 'enum') { propertyDefinition.enum = field.querySelector('textarea').value.split('\n').filter(v => v.trim()); } // Procesar y añadir el valor por defecto si se proporcionó if (defaultValueString !== null && defaultValueString.trim() !== '') { let typedDefaultValue = defaultValueString; try { if (propertyDefinition.type === 'number' || propertyDefinition.type === 'integer') { typedDefaultValue = Number(defaultValueString); if (isNaN(typedDefaultValue)) { console.warn(`Valor por defecto inválido para número en campo '${key}': ${defaultValueString}. Se omitirá.`); // No añadir default si no es un número válido } else { // Opcional: truncar si el tipo es integer if (propertyDefinition.type === 'integer' && !Number.isInteger(typedDefaultValue)) { typedDefaultValue = Math.trunc(typedDefaultValue); } propertyDefinition.default = typedDefaultValue; } } else if (propertyDefinition.type === 'boolean') { typedDefaultValue = ['true', '1', 'yes', 'on'].includes(defaultValueString.toLowerCase()); propertyDefinition.default = typedDefaultValue; } else { // string, enum, directory propertyDefinition.default = typedDefaultValue; // Ya es string } } catch (e) { console.error(`Error procesando valor por defecto para campo '${key}':`, e); } } schema.properties[key] = propertyDefinition; }); 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); } // Modificar initWorkingDirectory para cargar también el historial 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); } await loadDirectoryHistory(); } 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 try { 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 y lista de directorios document.getElementById('working-directory').value = path; await loadDirectoryHistory(); // 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')); } } catch (error) { console.error('Error updating working directory:', error); alert('Error actualizando el directorio de trabajo: ' + error.message); } } async function loadDirectoryHistory() { try { const response = await fetch(`/api/directory-history/${currentGroup}`); const history = await response.json(); const select = document.getElementById('directory-history'); select.innerHTML = ''; history.forEach(dir => { const option = document.createElement('option'); option.value = dir; option.textContent = dir; // Marcar como seleccionado si es el directorio actual if (dir === document.getElementById('working-directory').value) { option.selected = true; } select.appendChild(option); }); } catch (error) { console.error('Error loading directory history:', error); } } function loadHistoryDirectory(path) { if (path) { document.getElementById('working-directory').value = path; updateWorkingDirectory(path); // Cambiado de setWorkingDirectory a updateWorkingDirectory } } // 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); // Event listener para el nuevo botón de abrir en explorador const openInExplorerButton = document.getElementById('open-in-explorer-btn'); if (openInExplorerButton) { openInExplorerButton.addEventListener('click', openCurrentWorkingDirectoryInExplorer); } // Cargar datos iniciales updateGroupDescription(); await initWorkingDirectory(); await loadConfigs(); // Mostrar level3-content por defecto const level3Content = document.getElementById('level3-content'); if (level3Content) { level3Content.classList.remove('hidden'); const button = document.querySelector(`[onclick="toggleConfig('level3-content')"]`); if (button) { button.innerText = 'Ocultar Configuración'; } } } 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'); // 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 } } 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; } // Añade esta función al final de tu archivo static/js/script.js function shutdownServer() { if (confirm("¿Estás seguro de que quieres detener el servidor? La aplicación se cerrará.")) { fetch('/_shutdown', { method: 'POST', headers: { 'Content-Type': 'application/json' } }) .then(response => response.json()) .then(data => { if (data.status === 'success') { alert("El servidor se está deteniendo. Puede que necesites cerrar esta pestaña manualmente."); // Opcionalmente, puedes intentar cerrar la ventana/pestaña // window.close(); // Esto puede no funcionar en todos los navegadores por seguridad document.body.innerHTML = '
El servidor se ha detenido. Cierra esta ventana.
'; } else { alert("Error al intentar detener el servidor: " + data.message); } }) .catch(error => { // Es normal recibir un error de red aquí porque el servidor se está apagando console.warn("Error esperado al detener el servidor (puede que ya se haya detenido):", error); alert("Solicitud de detención enviada. El servidor debería detenerse. Cierra esta ventana."); document.body.innerHTML = '
El servidor se está deteniendo. Cierra esta ventana.
'; }); } } // Asegúrate de que las funciones fetchLogs y clearLogs también estén definidas en este archivo si las usas. // Ejemplo de fetchLogs y clearLogs (si no las tienes ya): function fetchLogs() { fetch('/api/logs') .then(response => response.json()) .then(data => { const logOutput = document.getElementById('log-area'); // Corregido ID a log-area logOutput.innerHTML = data.logs || 'No hay logs.'; // Usar innerHTML para mantener formato si existe logOutput.scrollTop = logOutput.scrollHeight; // Scroll to bottom }) .catch(error => console.error('Error fetching logs:', error)); } function clearLogs() { if (confirm("¿Estás seguro de que quieres borrar los logs?")) { fetch('/api/logs', { method: 'DELETE' }) .then(response => response.json()) .then(data => { if (data.status === 'success') { // Limpiar el área de log visualmente AHORA document.getElementById('log-area').innerHTML = ''; showToast('Logs borrados correctamente.'); } else { showToast('Error al borrar los logs.', 'error'); } }) .catch(error => { console.error('Error clearing logs:', error); showToast('Error de red al borrar los logs.', 'error'); }); } } // Necesitarás una función showToast o similar si la usas function showToast(message, type = 'success') { // Implementa tu lógica de Toast aquí console.log(`UI (${type}): ${message}`); // Siempre loguea en consola if (type === 'error') { alert(`Error: ${message}`); // Muestra alerta solo para errores } } // Llama a fetchLogs al cargar la página si es necesario // document.addEventListener('DOMContentLoaded', fetchLogs); // 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); } } async function openGroupInEditor(editorCode, groupSystem, groupId) { // groupId is already the currentGroup string from the select if (!groupId) { alert('Por favor, seleccione un grupo de scripts primero'); return; } const editorName = editorCode.toUpperCase(); try { const response = await fetch(`/api/open-editor/${editorCode}/${groupSystem}/${groupId}`, { method: 'POST' }); if (!response.ok) { // If response is not OK, it might not be JSON (e.g., Flask error page) const errorText = await response.text(); console.error(`Error al abrir ${editorName}: HTTP ${response.status}`, errorText); alert(`Error al abrir ${editorName}: ${response.status} ${response.statusText}\n${errorText.substring(0, 200)}...`); // Show limited error text } else { const result = await response.json(); // Now it's safer to parse JSON if (result.status === 'success') { console.log(`${editorName} opened successfully`); } else { console.error(`Error al abrir ${editorName}:`, result.message); alert(`Error al abrir ${editorName}: ${result.message}`); } } } catch (error) { console.error(`Error en la llamada fetch para open-editor (${editorName}):`, error); alert(`Error de red o del cliente al intentar abrir ${editorName}.`); } } function openMinicondaConsole() { fetch('/api/open-miniconda', { method: 'POST', headers: { 'Content-Type': 'application/json' } }) .then(response => response.json()) .then(data => { if (data.status === 'success') { showNotification('Miniconda Console abierta correctamente', 'success'); } else { showNotification(`Error al abrir Miniconda Console: ${data.message}`, 'error'); } }) .catch(error => { console.error('Error opening Miniconda Console:', error); showNotification('Error al comunicarse con el servidor', 'error'); }); } async function openCurrentWorkingDirectoryInExplorer() { const group = currentGroup; // Asumiendo que currentGroup está disponible globalmente const wdInput = document.getElementById('working-directory'); const path = wdInput.value; if (!group) { showToast("Por favor, selecciona un grupo primero.", "warning"); // O usa alert() si showToast no está definida // alert("Por favor, selecciona un grupo primero."); return; } if (!path || path.trim() === "") { showToast("El directorio de trabajo no está establecido.", "warning"); // alert("El directorio de trabajo no está establecido."); return; } try { const response = await fetch('/api/open-explorer', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ path: path, group: group }) }); const result = await response.json(); if (result.status === "success") { // No es necesario un toast para éxito, la acción es visible } else { showToast(result.message || "Error al intentar abrir el explorador.", "error"); } } catch (error) { console.error("Error de red al abrir en explorador:", error); showToast("Error de red al intentar abrir el explorador.", "error"); } }