Funcionando solo problemas con el log que repite las lineas. Integrado CronoEmail

This commit is contained in:
Miguel 2025-02-09 19:33:03 +01:00
parent 930e578cec
commit 16bc9f9390
11 changed files with 367 additions and 225 deletions

33
app.py
View File

@ -1,6 +1,7 @@
from flask import Flask, render_template, request, jsonify, url_for
from flask_sock import Sock
from config_manager import ConfigurationManager
from datetime import datetime
import os
import json # Added import
@ -31,14 +32,32 @@ def handle_websocket(ws):
def broadcast_message(message):
"""Envía un mensaje a todas las conexiones WebSocket activas y guarda en log."""
dead_connections = set()
for ws in websocket_connections:
try:
ws.send(message)
except Exception:
dead_connections.add(ws)
timestamp = datetime.now().strftime("[%H:%M:%S] ")
# Guardar en archivo de log
config_manager.append_log(message)
# Si es una lista de mensajes, procesar cada uno
if isinstance(message, list):
messages = message
else:
# Si es un solo mensaje, dividirlo en líneas
messages = [line.strip() for line in message.splitlines() if line.strip()]
# Procesar cada mensaje
for msg in messages:
# Verificar si el mensaje ya tiene timestamp
has_timestamp = msg.startswith("[") and "]" in msg.split(" ")[0]
formatted_msg = msg if has_timestamp else f"{timestamp}{msg}"
# Escribir en el archivo de log
with open(config_manager.log_file, "a", encoding="utf-8") as f:
f.write(f"{formatted_msg}\n")
# Enviar a todos los clientes WebSocket
for ws in list(websocket_connections):
try:
if ws.connected:
ws.send(f"{formatted_msg}\n")
except Exception:
dead_connections.add(ws)
# Limpiar conexiones muertas
websocket_connections.difference_update(dead_connections)

View File

@ -1,6 +0,0 @@
{
"input_dir": "C:\\Trabajo\\VM\\40 - 93040 - HENKEL - NEXT2 Problem\\Reporte\\Emails",
"output_dir": "C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\Trabajo\\VM\\04-InLavoro\\HENKEL\\93040 - HENKEL - BowlingGreen\\Description\\HENKEL - ALPLA - AUTEFA - Batch Data",
"cronologia_file": "cronologia.md",
"attachments_dir": "adjuntos"
}

View File

@ -1,5 +1,4 @@
{
"attachments_dir": "adjuntos",
"cronologia_file": "cronologia.md",
"output_dir": "C:\\\\Users\\\\migue\\\\OneDrive\\\\Miguel\\\\Obsidean\\\\Trabajo\\\\VM\\\\04-InLavoro\\\\HENKEL\\\\93040 - HENKEL - BowlingGreen\\\\Description\\\\HENKEL - ALPLA - AUTEFA - Batch Data"
"cronologia_file": "cronologia.md"
}

View File

@ -1,10 +1,11 @@
{
"type": "object",
"properties": {
"output_dir": {
"output_directory": {
"type": "string",
"title": "Directorio de destino de la cronologia",
"description": ""
"format": "directory",
"title": "Directorio donde escribir el archivo de cronologia",
"description": "Lugar para el archivo cronologia.md"
}
}
}

View File

@ -1,15 +1,18 @@
"""
Script para dessasemblar los emails y generar un archivo de texto con la cronología de los mensajes.
Script para desensamblar los emails y generar un archivo de texto con la cronología de los mensajes.
"""
# main.py
import os
import sys
from pathlib import Path
from utils.email_parser import procesar_eml
from utils.markdown_handler import cargar_cronologia_existente
from utils.beautify import BeautifyProcessor
from config.config import Config
import hashlib
import json
# Forzar UTF-8 en la salida estándar
sys.stdout.reconfigure(encoding="utf-8")
def generar_indice(mensajes):
"""
@ -23,33 +26,52 @@ def generar_indice(mensajes):
indice += "\n---\n\n"
return indice
def main():
config = Config()
# Cargar configuraciones del entorno
configs = json.loads(os.environ.get("SCRIPT_CONFIGS", "{}"))
# Obtener working directory
working_directory = configs.get("working_directory", ".")
# Obtener configuraciones de nivel 2 (grupo)
group_config = configs.get("level2", {})
cronologia_file = group_config.get("cronologia_file", "cronologia.md")
attachments_dir = group_config.get("attachments_dir", "adjuntos")
work_config = configs.get("level3", {})
outpu_directory = work_config.get("output_directory", ".")
# Construir rutas absolutas
input_dir = (
working_directory # El directorio de trabajo es el directorio de entrada
)
output_file = os.path.join(outpu_directory, cronologia_file)
attachments_path = os.path.join(working_directory, attachments_dir)
# Debug prints
print(f"Input directory: {config.get_input_dir()}")
print(f"Output directory: {config.get_output_dir()}")
print(f"Cronologia file: {config.get_cronologia_file()}")
print(f"Attachments directory: {config.get_attachments_dir()}")
print(f"Working directory: {working_directory}")
print(f"Input directory: {input_dir}")
print(f"Output directory: {outpu_directory}")
print(f"Cronologia file: {output_file}")
print(f"Attachments directory: {attachments_path}")
# Obtener el directorio donde está el script actual
script_dir = os.path.dirname(os.path.abspath(__file__))
# Construir la ruta al archivo de reglas en el subdirectorio config
beautify_rules = os.path.join(script_dir, "config", "beautify_rules.json")
beautifier = BeautifyProcessor(beautify_rules)
print(f"Beautify rules file: {beautify_rules}")
# Ensure directories exist
os.makedirs(config.get_output_dir(), exist_ok=True)
os.makedirs(config.get_attachments_dir(), exist_ok=True)
os.makedirs(attachments_path, exist_ok=True)
# Check if input directory exists and has files
input_path = Path(config.get_input_dir())
input_path = Path(input_dir)
if not input_path.exists():
print(f"Error: Input directory {input_path} does not exist")
return
eml_files = list(input_path.glob('*.eml'))
eml_files = list(input_path.glob("*.eml"))
print(f"Found {len(eml_files)} .eml files")
mensajes = []
@ -62,7 +84,7 @@ def main():
for archivo in eml_files:
print(f"\nProcessing {archivo}")
nuevos_mensajes = procesar_eml(archivo, config.get_attachments_dir())
nuevos_mensajes = procesar_eml(archivo, attachments_path)
total_procesados += len(nuevos_mensajes)
# Verificar duplicados y aplicar beautify solo a los mensajes nuevos
@ -76,10 +98,10 @@ def main():
else:
mensajes_duplicados += 1
print(f"\nEstadísticas de procesamiento:")
print(f"- Total mensajes encontrados: {total_procesados}")
print(f"- Mensajes únicos añadidos: {total_nuevos}")
print(f"- Mensajes duplicados ignorados: {mensajes_duplicados}")
print("\nEstadísticas de procesamiento:")
print("- Total mensajes encontrados:", total_procesados)
print("- Mensajes únicos añadidos:", total_nuevos)
print("- Mensajes duplicados ignorados:", mensajes_duplicados)
# Ordenar mensajes de más reciente a más antiguo
mensajes.sort(key=lambda x: x.fecha, reverse=True)
@ -88,14 +110,14 @@ def main():
indice = generar_indice(mensajes)
# Escribir el archivo con el índice y los mensajes
output_file = config.get_cronologia_file()
print(f"\nWriting {len(mensajes)} messages to {output_file}")
with open(output_file, 'w', encoding='utf-8') as f:
with open(output_file, "w", encoding="utf-8") as f:
# Primero escribir el índice
f.write(indice)
# Luego escribir todos los mensajes
for msg in mensajes:
f.write(msg.to_markdown())
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@ -0,0 +1,4 @@
{
"type": "object",
"properties": {}
}

View File

@ -0,0 +1,4 @@
{
"type": "object",
"properties": {}
}

View File

@ -3,6 +3,8 @@ import json
import subprocess
import re
from typing import Dict, Any, List
import time # Add this import
from datetime import datetime # Add this import
class ConfigurationManager:
@ -15,6 +17,8 @@ class ConfigurationManager:
self.working_directory = None
self.log_file = os.path.join(self.data_path, "log.txt")
self._init_log_file()
self.last_execution_time = 0 # Add this attribute
self.min_execution_interval = 1 # Minimum seconds between executions
def _init_log_file(self):
"""Initialize log file if it doesn't exist"""
@ -25,13 +29,39 @@ class ConfigurationManager:
f.write("")
def append_log(self, message: str) -> None:
"""Append a message to the log file"""
"""Append a message to the log file with timestamp."""
try:
with open(self.log_file, "a", encoding="utf-8") as f:
f.write(message)
timestamp = datetime.now().strftime("[%H:%M:%S] ")
# Filtrar líneas vacías y agregar timestamp solo a líneas con contenido
lines = message.split("\n")
lines_with_timestamp = []
for line in lines:
if line.strip():
# Solo agregar timestamp si la línea no tiene ya uno
if not line.strip().startswith("["):
line = f"{timestamp}{line}"
lines_with_timestamp.append(f"{line}\n")
if lines_with_timestamp:
with open(self.log_file, "a", encoding="utf-8") as f:
f.writelines(lines_with_timestamp)
except Exception as e:
print(f"Error writing to log file: {e}")
def read_last_log_line(self) -> str:
"""Read the last line from the log file."""
try:
with open(self.log_file, "r", encoding="utf-8") as f:
# Leer las últimas líneas y encontrar la última no vacía
lines = f.readlines()
for line in reversed(lines):
if line.strip():
return line
return ""
except Exception as e:
print(f"Error reading last log line: {e}")
return ""
def read_log(self) -> str:
"""Read the entire log file"""
try:
@ -124,7 +154,9 @@ class ConfigurationManager:
if level == "1":
path = os.path.join(self.data_path, "esquema_general.json")
elif level == "2":
path = os.path.join(self.script_groups_path, group, "esquema_group.json")
path = os.path.join(
self.script_groups_path, group, "esquema_group.json"
)
elif level == "3":
if not group:
return {"type": "object", "properties": {}}
@ -331,64 +363,89 @@ class ConfigurationManager:
self, group: str, script_name: str, broadcast_fn=None
) -> Dict[str, Any]:
"""Execute script with real-time logging via WebSocket broadcast function."""
script_path = os.path.join(self.script_groups_path, group, script_name)
if not os.path.exists(script_path):
return {"error": "Script not found"}
# Check execution throttling
current_time = time.time()
time_since_last = current_time - self.last_execution_time
# Obtener el directorio de trabajo del grupo
if time_since_last < self.min_execution_interval:
if broadcast_fn:
broadcast_fn(
f"Por favor espere {self.min_execution_interval} segundo(s) entre ejecuciones"
)
return {
"status": "throttled",
"error": f"Por favor espere {self.min_execution_interval} segundo(s) entre ejecuciones",
}
self.last_execution_time = current_time
script_path = os.path.join(self.script_groups_path, group, script_name)
if not os.path.exists(script_path):
if broadcast_fn:
broadcast_fn("Error: Script no encontrado")
return {"status": "error", "error": "Script not found"}
# Get working directory
working_dir = self.get_work_dir(group)
if not working_dir:
return {"error": "Working directory not set for this script group"}
if broadcast_fn:
broadcast_fn("Error: Directorio de trabajo no configurado")
return {"status": "error", "error": "Working directory not set"}
# Prepare environment configurations
configs = {
"level1": self.get_config("1"),
"level2": self.get_config("2", group),
"level3": self.get_config("3") if self.working_directory else {},
"level3": self.get_config("3", group) if working_dir else {},
"working_directory": working_dir,
}
try:
if broadcast_fn:
broadcast_fn(f"\nIniciando ejecución de {script_name}...\n")
broadcast_fn(f"Iniciando ejecución de {script_name}")
# Execute script with configured environment
process = subprocess.Popen(
["python", script_path],
cwd=working_dir or self.base_path,
cwd=working_dir,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
env=dict(os.environ, **{"SCRIPT_CONFIGS": json.dumps(configs)}),
env=dict(
os.environ,
SCRIPT_CONFIGS=json.dumps(configs),
PYTHONIOENCODING="utf-8",
),
)
output = []
# Stream output in real-time
while True:
line = process.stdout.readline()
if not line and process.poll() is not None:
break
if line:
output.append(line)
if broadcast_fn:
broadcast_fn(line)
if line and broadcast_fn:
broadcast_fn(line.rstrip())
# Handle any errors
stderr = process.stderr.read()
if stderr:
if broadcast_fn:
broadcast_fn(f"\nERROR: {stderr}\n")
output.append(f"ERROR: {stderr}")
if stderr and broadcast_fn:
broadcast_fn(f"ERROR: {stderr.strip()}")
# Signal completion
if broadcast_fn:
broadcast_fn("\nEjecución completada.\n")
broadcast_fn("Ejecución completada")
return {
"output": "".join(output),
"error": stderr if stderr else None,
"status": "success" if process.returncode == 0 else "error",
"error": stderr if stderr else None,
}
except Exception as e:
error_msg = str(e)
if broadcast_fn:
broadcast_fn(f"\nError inesperado: {error_msg}\n")
return {"error": error_msg}
broadcast_fn(f"Error inesperado: {error_msg}")
return {"status": "error", "error": error_msg}
def get_work_dir(self, group: str) -> str:
"""Get working directory path for a script group."""

View File

@ -1,62 +1,26 @@
Iniciando ejecución de x1.py...
=== Ejecutando Script de Prueba 1 ===
Configuraciones cargadas:
Nivel 1: {
"api_key": "your-api-key-here",
"model": "gpt-3.5-turbo"
}
Nivel 2: {
"input_dir": "D:/Datos/Entrada",
"output_dir": "D:/Datos/Salida",
"batch_size": 50
}
Nivel 3: {
"campo_1739099176331": "",
"debug_mode": false,
"process_type": "basic",
"project_name": "Test2"
}
Simulando procesamiento...
Progreso: 20%
Progreso: 40%
Progreso: 60%
Progreso: 80%
Progreso: 100%
¡Proceso completado!
Ejecución completada.
Iniciando ejecución de x1.py...
=== Ejecutando Script de Prueba 1 ===
Configuraciones cargadas:
Nivel 1: {
"api_key": "your-api-key-here",
"model": "gpt-3.5-turbo"
}
Nivel 2: {
"input_dir": "D:/Datos/Entrada",
"output_dir": "D:/Datos/Salida",
"batch_size": 50
}
Nivel 3: {
"campo_1739099176331": "",
"debug_mode": true,
"process_type": "basic",
"project_name": "Test2"
}
Simulando procesamiento...
Progreso: 20%
Progreso: 40%
Progreso: 60%
Progreso: 80%
Progreso: 100%
¡Proceso completado!
Ejecución completada.
[19:32:18] Iniciando ejecución de x1.py
[19:32:18] === Ejecutando Script de Prueba 1 ===
[19:32:18] Configuraciones cargadas:
[19:32:18] Nivel 1: {
[19:32:18] "api_key": "your-api-key-here",
[19:32:18] "model": "gpt-3.5-turbo"
[19:32:18] }
[19:32:18] Nivel 2: {
[19:32:18] "input_dir": "D:/Datos/Entrada",
[19:32:18] "output_dir": "D:/Datos/Salida",
[19:32:18] "batch_size": 50
[19:32:18] }
[19:32:18] Nivel 3: {
[19:32:18] "campo_1739099176331": "",
[19:32:18] "debug_mode": true,
[19:32:18] "process_type": "basic",
[19:32:18] "project_name": "Test2"
[19:32:18] }
[19:32:18] Simulando procesamiento...
[19:32:19] Progreso: 20%
[19:32:20] Progreso: 40%
[19:32:21] Progreso: 60%
[19:32:22] Progreso: 80%
[19:32:23] Progreso: 100%
[19:32:23] ¡Proceso completado!
[19:32:23] Ejecución completada

View File

@ -18,32 +18,44 @@ function initWebSocket() {
// 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
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...`); // Debug line
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); // Debug line
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...'); // Debug line
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); // Debug line
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);
}
@ -98,17 +110,34 @@ async function renderForm(containerId, data) {
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 onclick="saveConfig(${level})"
class="bg-green-500 text-white px-4 py-2 rounded">
<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>';
@ -163,6 +192,18 @@ function generateInputField(def, key, value, level) {
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('')}
@ -187,6 +228,27 @@ function generateInputField(def, key, value, level) {
}
}
// 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
@ -292,7 +354,8 @@ function createFieldEditor(key, field) {
<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="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>
@ -337,7 +400,7 @@ function updateFieldType(select) {
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>
onchange="updateVisualSchema()"></textarea>
`;
fieldContainer.appendChild(div);
}
@ -387,21 +450,29 @@ function updateVisualSchema() {
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());
}
if (select.value === 'directory') {
schema.properties[key] = {
type: 'string',
format: 'directory',
title: inputs[1].value,
description: inputs[2].value
};
} else if (select.value === 'enum') {
schema.properties[key] = {
type: 'string',
title: inputs[1].value,
description: inputs[2].value,
enum: field.querySelector('textarea').value.split('\n').filter(v => v.trim())
};
} else {
schema.properties[key] = {
type: select.value,
title: inputs[1].value,
description: inputs[2].value
};
}
});
// Actualizar el JSON editor directamente
const jsonEditor = document.getElementById('json-editor');
if (jsonEditor) {
jsonEditor.value = JSON.stringify(schema, null, 2);
@ -562,50 +633,59 @@ async function loadStoredLogs() {
// Initialize on page load
async function initializeApp() {
try {
// Inicializar WebSocket
initWebSocket();
await loadStoredLogs(); // Cargar logs almacenados
await loadStoredLogs();
// Primero establecer el grupo actual
const group = localStorage.getItem('selectedGroup');
// Configurar grupo actual
const selectElement = document.getElementById('script-group');
if (group) {
selectElement.value = group;
currentGroup = localStorage.getItem('selectedGroup') || selectElement.value;
// Actualizar el select con el valor guardado
if (currentGroup) {
selectElement.value = currentGroup;
}
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();
});
// Limpiar evento anterior si existe
selectElement.removeEventListener('change', handleGroupChange);
// Luego cargar el directorio de trabajo
// Agregar el nuevo manejador de eventos
selectElement.addEventListener('change', handleGroupChange);
// Cargar datos iniciales
updateGroupDescription();
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();
} 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 = '';
});
// Close sidebar on small screens when changing groups
// Actualizar la interfaz
updateGroupDescription();
await initWorkingDirectory();
await loadConfigs();
// Cerrar sidebar en móviles
if (window.innerWidth < 768) {
toggleSidebar();
}
} catch (error) {
console.error('Error during initialization:', error);
console.error('Error in handleGroupChange:', error);
}
}
@ -791,35 +871,19 @@ function collectFormData(level) {
// 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 {
const form = document.getElementById(`config-form-${level}`);
const formData = {};
saveButton.disabled = true;
saveButton.className = 'bg-yellow-500 text-white px-4 py-2 rounded cursor-wait transition-colors duration-300';
saveButton.innerText = 'Guardando...';
// Recolectar datos de todos los inputs en el formulario
form.querySelectorAll('input, select').forEach(input => {
const key = input.getAttribute('data-key');
if (!key) return;
const formData = collectFormData(level);
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' },
@ -832,16 +896,30 @@ async function saveConfig(level) {
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);
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);
alert('Error guardando la configuración: ' + error.message);
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);
}
}