1417 lines
57 KiB
JavaScript
1417 lines
57 KiB
JavaScript
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 = '<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">
|
|
<button data-filename="${script.filename}"
|
|
class="bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded text-sm w-24 text-center execute-button">
|
|
Ejecutar
|
|
</button>
|
|
<div class="text-xs text-gray-500 mt-1 truncate w-24 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 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 = '<p class="text-gray-500">No hay esquema definido para este nivel.</p>';
|
|
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 = `
|
|
<form id="config-form-${level}" class="space-y-4">
|
|
${generateFormFields(schema, data || {}, '', level)}
|
|
</form>
|
|
<div class="flex justify-end mt-4">
|
|
<button id="save-config-${level}" onclick="saveConfig(${level})"
|
|
class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded transition-colors duration-300">
|
|
Guardar Configuración
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
// 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 = '<p class="text-red-500">Error cargando el esquema.</p>';
|
|
}
|
|
}
|
|
|
|
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 += `<div class="mb-4">
|
|
<label class="block text-gray-700 text-sm font-bold mb-2">${def.title || key}</label>`;
|
|
|
|
if (def.type === 'object') {
|
|
html += `<div class="pl-4 border-l-2 border-gray-200">
|
|
${generateFormFields(def, data, fullKey, level)}
|
|
</div>`;
|
|
} else {
|
|
html += generateInputField(def, fullKey, value, level);
|
|
}
|
|
|
|
if (def.description) {
|
|
html += `<p class="text-gray-500 text-xs mt-1">${def.description}</p>`;
|
|
}
|
|
html += '</div>';
|
|
}
|
|
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 `<div class="flex gap-2">
|
|
<input type="text" value="${value || ''}"
|
|
class="${baseClasses} flex-grow" data-key="${key}">
|
|
<button type="button"
|
|
onclick="browseFieldDirectory(this)"
|
|
class="bg-blue-500 text-white px-3 py-1 rounded hover:bg-blue-600"
|
|
data-key="${key}">
|
|
Buscar...
|
|
</button>
|
|
</div>`;
|
|
}
|
|
if (def.enum) {
|
|
return `<select class="${baseClasses}" data-key="${key}">
|
|
${def.enum.map(opt => `<option value="${opt}" ${value === opt ? 'selected' : ''}>${opt}</option>`).join('')}
|
|
</select>`;
|
|
}
|
|
return `<input type="text" value="${value || ''}"
|
|
class="${baseClasses}" data-key="${key}">`;
|
|
|
|
case 'number':
|
|
return `<input type="number" value="${value || 0}"
|
|
class="${baseClasses}" data-key="${key}">`;
|
|
|
|
case 'boolean':
|
|
return `<input type="checkbox" ${value ? 'checked' : ''}
|
|
class="form-checkbox h-5 w-5 bg-green-50" data-key="${key}">
|
|
`; // <-- Añadir esta comilla invertida
|
|
default:
|
|
return `<input type="text" value="${value || ''}"
|
|
class="${baseClasses}" data-key="${key}">`;
|
|
}
|
|
}
|
|
|
|
// 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 = '<div id="schema-fields" class="mb-4"></div>' +
|
|
'<button onclick="addSchemaField()" class="mt-4 bg-green-500 text-white px-4 py-2 rounded">Agregar Campo</button>';
|
|
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 = `
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-bold mb-2">Nombre del Campo</label>
|
|
<input type="text" value="${key}"
|
|
class="w-full p-2 border rounded"
|
|
onchange="updateVisualSchema()">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-bold mb-2">Tipo</label>
|
|
<select class="w-full p-2 border rounded"
|
|
onchange="updateFieldType(this)">
|
|
<option value="string" ${field.type === 'string' && !field.enum && !field.format ? 'selected' : ''}>Texto</option>
|
|
<option value="directory" ${field.type === 'string' && field.format === 'directory' ? 'selected' : ''}>Directorio</option>
|
|
<option value="number" ${field.type === 'number' ? 'selected' : ''}>Número</option>
|
|
<option value="boolean" ${field.type === 'boolean' ? 'selected' : ''}>Booleano</option>
|
|
<option value="enum" ${field.enum ? 'selected' : ''}>Lista de Opciones</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-bold mb-2">Título</label>
|
|
<input type="text" value="${field.title || ''}"
|
|
class="w-full p-2 border rounded"
|
|
onchange="updateVisualSchema()">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-bold mb-2">Descripción</label>
|
|
<input type="text" value="${field.description || ''}"
|
|
class="w-full p-2 border rounded"
|
|
onchange="updateVisualSchema()">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-bold mb-2">Valor por Defecto</label>
|
|
<input type="text" value="${field.default !== undefined ? field.default : ''}"
|
|
class="w-full p-2 border rounded"
|
|
onchange="updateVisualSchema()">
|
|
</div>
|
|
</div>
|
|
${field.enum ? `
|
|
<div class="enum-container mt-4">
|
|
<label class="block text-sm font-bold mb-2">Opciones (una por línea)</label>
|
|
<textarea class="w-full p-2 border rounded" rows="3"
|
|
onchange="updateVisualSchema()">${field.enum.join('\n')}</textarea>
|
|
</div>
|
|
` : ''}
|
|
<button onclick="removeField(this)"
|
|
class="mt-2 bg-red-500 text-white px-3 py-1 rounded">
|
|
Eliminar Campo
|
|
</button>
|
|
`;
|
|
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 = `
|
|
<label class="block text-sm font-bold mb-2">Opciones (una por línea)</label>
|
|
<textarea class="w-full p-2 border rounded" rows="3"
|
|
onchange="updateVisualSchema()"></textarea>
|
|
`;
|
|
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 `
|
|
<div class="mt-4">
|
|
<label class="block text-sm font-bold mb-2">Opciones (una por línea)</label>
|
|
<textarea class="w-full p-2 border rounded" rows="3"
|
|
onchange="updateEnumValues(this)">${enumValues.join('\n')}</textarea>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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 = '<option value="">-- Directorios recientes --</option>';
|
|
|
|
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() {
|
|
// Eliminada la confirmación - directamente procede a limpiar
|
|
const response = await fetch('/api/logs', { method: 'DELETE' });
|
|
const result = await response.json();
|
|
if (result.status === 'success') {
|
|
document.getElementById('log-area').innerHTML = '';
|
|
showToast ? showToast('Logs borrados correctamente', 'success') : console.log('Logs cleared');
|
|
} else {
|
|
showToast ? showToast('Error al borrar los logs', 'error') : alert('Error al borrar los logs');
|
|
}
|
|
}
|
|
|
|
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 = `
|
|
<form id="group-description-form" class="grid gap-4">
|
|
<div>
|
|
<label class="block text-sm font-bold mb-2">Nombre del Grupo</label>
|
|
<input type="text" name="name" class="w-full p-2 border rounded"
|
|
value="${description.name || ''}" placeholder="Nombre del grupo">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-bold mb-2">Descripción</label>
|
|
<textarea name="description" class="w-full p-2 border rounded" rows="3"
|
|
placeholder="Descripción detallada del grupo">${description.description || ''}</textarea>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-bold mb-2">Versión</label>
|
|
<input type="text" name="version" class="w-full p-2 border rounded"
|
|
value="${description.version || '1.0'}" placeholder="1.0">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-bold mb-2">Autor</label>
|
|
<input type="text" name="author" class="w-full p-2 border rounded"
|
|
value="${description.author || ''}" placeholder="Nombre del autor">
|
|
</div>
|
|
</div>
|
|
</form>
|
|
`;
|
|
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 = '<div class="alert alert-info">El servidor se ha detenido. Cierra esta ventana.</div>';
|
|
} 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 = '<div class="alert alert-info">El servidor se está deteniendo. Cierra esta ventana.</div>';
|
|
});
|
|
}
|
|
}
|
|
|
|
// 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));
|
|
}
|
|
|
|
// 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");
|
|
}
|
|
}
|
|
|
|
async function openGroupFolder(groupSystem, groupId) {
|
|
if (!groupId) {
|
|
alert('Por favor, seleccione un grupo de scripts primero');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/open-group-folder/${groupSystem}/${groupId}`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
const result = await response.json();
|
|
if (result.status === 'success') {
|
|
console.log(`Carpeta abierta: ${result.path}`);
|
|
} else {
|
|
alert(`Error al abrir carpeta: ${result.message}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error opening folder:', error);
|
|
alert('Error al comunicarse con el servidor');
|
|
}
|
|
}
|
|
|
|
async function copyGroupPath(groupSystem, groupId) {
|
|
if (!groupId) {
|
|
alert('Por favor, seleccione un grupo de scripts primero');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/get-group-path/${groupSystem}/${groupId}`);
|
|
const result = await response.json();
|
|
|
|
if (result.status === 'success') {
|
|
// Copiar al portapapeles usando la API moderna
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
await navigator.clipboard.writeText(result.path);
|
|
showToast ? showToast('Path copiado al portapapeles', 'success') : alert('Path copiado al portapapeles');
|
|
} else {
|
|
// Fallback para navegadores más antiguos
|
|
const textArea = document.createElement('textarea');
|
|
textArea.value = result.path;
|
|
document.body.appendChild(textArea);
|
|
textArea.focus();
|
|
textArea.select();
|
|
|
|
try {
|
|
document.execCommand('copy');
|
|
showToast ? showToast('Path copiado al portapapeles', 'success') : alert('Path copiado al portapapeles');
|
|
} catch (err) {
|
|
console.error('Error copying to clipboard:', err);
|
|
alert(`Error al copiar. Path: ${result.path}`);
|
|
}
|
|
|
|
document.body.removeChild(textArea);
|
|
}
|
|
} else {
|
|
alert(`Error al obtener path: ${result.message}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error getting path:', error);
|
|
alert('Error al comunicarse con el servidor');
|
|
}
|
|
} |