ParamManagerScripts/templates/index.html

1156 lines
50 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Script Parameter Manager</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
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 = document.getElementById('script-group').value;
currentGroup = group;
console.log('Loading configs for group:', group); // Debug line
try {
// Cargar niveles 1 y 2
for (let level of [1, 2]) {
console.log(`Loading level ${level} config...`); // Debug line
const response = await fetch(`/api/config/${level}?group=${group}`);
const data = await response.json();
console.log(`Level ${level} data:`, data); // Debug line
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...'); // Debug line
const response = await fetch(`/api/config/3?group=${group}`);
const data = await response.json();
console.log('Level 3 data:', data); // Debug line
await renderForm('level3-form', data);
}
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 => `
<div class="mb-4 p-4 border rounded">
<div class="font-bold">${script.name}</div>
<div class="text-gray-600 text-sm">${script.description}</div>
<button onclick="executeScript('${script.name}')"
class="mt-2 bg-green-500 text-white px-3 py-1 rounded">
Ejecutar
</button>
</div>
`).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 = '<p class="text-gray-500">No hay esquema definido para este nivel.</p>';
return;
}
container.innerHTML = `
<form id="config-form-${level}" class="space-y-4">
${generateFormFields(schema, data || {}, '', level)}
</form>
<div class="flex justify-end mt-4">
<button onclick="saveConfig(${level})"
class="bg-green-500 text-white px-4 py-2 rounded">
Guardar Configuración
</button>
</div>
`;
} 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.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 `<div class="flex items-center">
<input type="checkbox" ${value ? 'checked' : ''}
class="form-checkbox h-5 w-5 bg-green-50" data-key="${key}">
</div>`;
default:
return `<input type="text" value="${value || ''}"
class="${baseClasses}" data-key="${key}">`;
}
}
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' ? 'selected' : ''}>Texto</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>
${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="updateEnumValues(this)"></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;
schema.properties[key] = {
type: select.value === 'enum' ? 'string' : select.value,
title: inputs[1].value,
description: inputs[2].value
};
if (select.value === 'enum') {
const textarea = field.getElementsByTagName('textarea')[0];
if (textarea) {
schema.properties[key].enum = textarea.value.split('\n').filter(v => v.trim());
}
}
});
// Actualizar el JSON editor directamente
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 {
initWebSocket();
await loadStoredLogs(); // Cargar logs almacenados
// Primero establecer el grupo actual
const group = localStorage.getItem('selectedGroup');
const selectElement = document.getElementById('script-group');
if (group) {
selectElement.value = group;
}
currentGroup = selectElement.value; // Siempre establecer currentGroup con el valor actual del select
console.log('Current group initialized as:', currentGroup); // Debug line
updateGroupDescription(); // Actualizar descripción inicial
// Configurar el evento de cambio de grupo
selectElement.addEventListener('change', async (e) => {
currentGroup = e.target.value;
localStorage.setItem('selectedGroup', e.target.value);
console.log('Group changed to:', currentGroup); // Debug line
updateGroupDescription(); // Actualizar descripción al cambiar
await initWorkingDirectory();
await loadConfigs();
});
// Luego cargar el directorio de trabajo
await initWorkingDirectory();
// Finalmente cargar las configuraciones
await loadConfigs();
// Configurar el evento de cambio de grupo
selectElement.addEventListener('change', async (e) => {
currentGroup = e.value;
localStorage.setItem('selectedGroup', e.value);
console.log('Group changed to:', currentGroup); // Debug line
await initWorkingDirectory();
await loadConfigs();
});
// Close sidebar on small screens when changing groups
if (window.innerWidth < 768) {
toggleSidebar();
}
} catch (error) {
console.error('Error during initialization:', 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 = `
<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;
}
// Agregar función para guardar configuración
async function saveConfig(level) {
try {
const form = document.getElementById(`config-form-${level}`);
const formData = {};
// Recolectar datos de todos los inputs en el formulario
form.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 = formData;
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;
});
// Enviar datos al servidor
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') {
alert('Configuración guardada correctamente');
// Recargar el formulario para mostrar los datos actualizados
const configResponse = await fetch(`/api/config/${level}?group=${currentGroup}`);
const updatedData = await configResponse.json();
await renderForm(`level${level}-form`, updatedData);
} else {
throw new Error(result.message || 'Error desconocido');
}
} catch (error) {
console.error('Error saving config:', error);
alert('Error guardando la configuración: ' + error.message);
}
}
</script>
<style>
.sidebar {
position: fixed;
top: 0;
right: -400px;
width: 400px;
height: 100vh;
background: white;
box-shadow: -2px 0 5px rgba(0,0,0,0.1);
transition: right 0.3s ease;
z-index: 40;
overflow-y: auto;
}
.sidebar.open {
right: 0;
}
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0,0,0,0.5);
display: none;
z-index: 30;
}
.overlay.show {
display: block;
}
#schema-editor {
z-index: 50;
}
#schema-editor .modal-content {
z-index: 51;
}
/* Reducción general de padding */
.container {
padding-left: 1.5rem !important; /* 30% menos que 4rem */
padding-right: 1.5rem !important;
}
.p-6 {
padding: 1.1rem !important; /* 30% menos que 1.5rem */
}
.px-4 {
padding-left: 0.7rem !important; /* 30% menos que 1rem */
padding-right: 0.7rem !important;
}
.py-8 {
padding-top: 1.5rem !important; /* 30% menos que 2rem */
padding-bottom: 1.5rem !important;
}
/* Ajustes para el log */
#log-area {
font-size: 0.7rem !important;
line-height: 1.2 !important;
padding: 0.7rem !important;
}
/* Ajustes para el modal */
#schema-editor .modal-content {
width: 50% !important;
max-width: 800px;
}
.mb-4 {
margin-bottom: 0.7rem !important; /* 30% menos que 1rem */
}
.mb-6 {
margin-bottom: 1.1rem !important; /* 30% menos que 1.5rem */
}
.mb-8 {
margin-bottom: 1.5rem !important; /* 30% menos que 2rem */
}
.mt-4 {
margin-top: 0.7rem !important;
}
.modal-header {
position: sticky;
top: 0;
background: white;
z-index: 52;
padding: 1rem;
border-bottom: 1px solid #e5e7eb;
}
.modal-footer {
position: sticky;
bottom: 0;
background: white;
z-index: 52;
padding: 1rem;
border-top: 1px solid #e5e7eb;
}
</style>
</head>
<body class="bg-gray-100">
<!-- Settings Button -->
<div class="fixed top-4 right-4 z-50">
<button onclick="toggleSidebar()" class="bg-white p-2 rounded-full shadow-lg hover:bg-gray-100">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
</button>
</div>
<!-- Sidebar -->
<div class="overlay" onclick="toggleSidebar()"></div>
<div class="sidebar">
<div class="p-6">
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-bold">Configuración Global</h2>
<button onclick="toggleSidebar()" class="text-gray-500 hover:text-gray-700">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<!-- Level 1 Configuration -->
<div class="mb-8">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Configuración General (Nivel 1)</h2>
<button class="bg-blue-500 text-white px-4 py-2 rounded"
onclick="toggleConfig('level1-content')">
Mostrar Configuración
</button>
</div>
<div id="level1-content" class="hidden">
<div id="level1-form"></div>
<div class="flex justify-between mt-4">
<button class="bg-blue-500 text-white px-4 py-2 rounded" onclick="modifySchema(1)">
Modificar Esquema
</button>
</div>
</div>
</div>
<!-- Level 2 Configuration -->
<div class="mb-8">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Configuración del Grupo (Nivel 2)</h2>
<button class="bg-blue-500 text-white px-4 py-2 rounded"
onclick="toggleConfig('level2-content')">
Mostrar Configuración
</button>
</div>
<div id="level2-content" class="hidden">
<div id="level2-form"></div>
<div class="flex justify-between mt-4">
<button class="bg-blue-500 text-white px-4 py-2 rounded" onclick="modifySchema(2)">
Modificar Esquema
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Main Content -->
<div class="container mx-auto px-4 py-8">
<!-- Script Group Selection -->
<div class="mb-8 bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-bold mb-4">Grupo de Scripts</h2>
<div class="flex gap-2 items-center">
<select id="script-group" class="flex-1 p-2 border rounded mb-2">
{% for group in script_groups %}
<option value="{{ group.id }}" data-description="{{ group.description }}">{{ group.name }}</option>
{% endfor %}
</select>
<button onclick="editGroupDescription()" class="bg-blue-500 text-white p-2 rounded mb-2">
<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>
<p id="group-description" class="text-gray-600 text-sm italic"></p>
<div class="text-xs text-gray-500 mt-2">
<span id="group-version"></span>
<span id="group-author"></span>
</div>
</div>
<!-- Working Directory -->
<div class="mb-8 bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-bold mb-4">Directorio de Trabajo</h2>
<div class="flex gap-4">
<div class="flex-1 flex gap-2">
<input type="text" id="working-directory" class="flex-1 p-2 border rounded bg-green-50">
<button class="bg-gray-500 text-white px-4 py-2 rounded" onclick="browseDirectory()">
Explorar
</button>
</div>
<button class="bg-blue-500 text-white px-4 py-2 rounded" onclick="setWorkingDirectory()">
Confirmar
</button>
</div>
</div>
<!-- Level 3 Configuration -->
<div class="mb-8 bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Configuración del Directorio (Nivel 3)</h2>
<button class="bg-blue-500 text-white px-4 py-2 rounded"
onclick="toggleConfig('level3-content')">
Mostrar Configuración
</button>
</div>
<div id="level3-content" class="hidden">
<div id="level3-form"></div>
<div class="flex justify-between mt-4">
<button class="bg-blue-500 text-white px-4 py-2 rounded" onclick="modifySchema(3)">
Modificar Esquema
</button>
</div>
</div>
</div>
<!-- Scripts List -->
<div class="mb-8 bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-bold mb-4">Scripts Disponibles</h2>
<div id="scripts-list"></div>
</div>
<!-- Logs -->
<div class="bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Logs</h2>
<button class="bg-red-500 text-white px-4 py-2 rounded" onclick="clearLogs()">
Limpiar
</button>
</div>
<div id="log-area" class="bg-gray-100 p-4 rounded h-64 overflow-y-auto font-mono text-sm whitespace-pre-wrap">
</div>
</div>
</div>
<!-- Schema Editor Modal -->
<div id="schema-editor" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center">
<div class="modal-content bg-white rounded-lg shadow-lg max-h-screen overflow-auto">
<div class="modal-header">
<div class="flex justify-between items-center">
<h3 class="text-xl font-bold">Editor de Esquema</h3>
<div class="flex gap-4">
<button onclick="document.getElementById('schema-editor').classList.add('hidden')"
class="bg-gray-500 text-white px-4 py-2 rounded">
Cancelar
</button>
<button onclick="saveSchema()"
class="bg-blue-500 text-white px-4 py-2 rounded">
Guardar
</button>
</div>
</div>
<div class="flex mt-4" id="editor-tabs">
<button id="visual-tab" class="px-4 py-2 border-b-2" onclick="switchEditorMode('visual')">Visual</button>
<button id="json-tab" class="px-4 py-2 border-b-2" onclick="switchEditorMode('json')">JSON</button>
</div>
</div>
<div class="p-4">
<div id="visual-editor" class="hidden">
<div id="schema-fields"></div>
<button onclick="addSchemaField()" class="mt-4 bg-green-500 text-white px-4 py-2 rounded">Agregar Campo</button>
</div>
<textarea id="json-editor" class="w-full h-96 font-mono p-2 border rounded"></textarea>
<input type="hidden" id="schema-level">
</div>
<div class="modal-footer">
<div class="flex justify-end gap-4">
<button onclick="document.getElementById('schema-editor').classList.add('hidden')"
class="bg-gray-500 text-white px-4 py-2 rounded">
Cancelar
</button>
<button onclick="saveSchema()"
class="bg-blue-500 text-white px-4 py-2 rounded">
Guardar
</button>
</div>
</div>
</div>
</div>
</body>
</html>