diff --git a/__pycache__/config_manager.cpython-310.pyc b/__pycache__/config_manager.cpython-310.pyc index a7dc8c1..ed725ab 100644 Binary files a/__pycache__/config_manager.cpython-310.pyc and b/__pycache__/config_manager.cpython-310.pyc differ diff --git a/app.py b/app.py index f4ea53b..755dca7 100644 --- a/app.py +++ b/app.py @@ -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) diff --git a/backend/script_groups/EmailCrono/config.json b/backend/script_groups/EmailCrono/config.json deleted file mode 100644 index f9b1ad9..0000000 --- a/backend/script_groups/EmailCrono/config.json +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/data.json b/backend/script_groups/EmailCrono/data.json index 7cac4f7..f9c99fb 100644 --- a/backend/script_groups/EmailCrono/data.json +++ b/backend/script_groups/EmailCrono/data.json @@ -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" } \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/esquema_work.json b/backend/script_groups/EmailCrono/esquema_work.json index 76dd02a..1b887a1 100644 --- a/backend/script_groups/EmailCrono/esquema_work.json +++ b/backend/script_groups/EmailCrono/esquema_work.json @@ -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" } } } \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/x1.py b/backend/script_groups/EmailCrono/x1.py index 73e2900..0fe3c86 100644 --- a/backend/script_groups/EmailCrono/x1.py +++ b/backend/script_groups/EmailCrono/x1.py @@ -1,70 +1,92 @@ """ -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): """ Genera una lista de mensajes usando el formato de Obsidian """ indice = "# Índice de Mensajes\n\n" - + for mensaje in mensajes: indice += mensaje.get_index_entry() + "\n" - + 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 = [] print(f"Loaded {len(mensajes)} existing messages") mensajes_hash = {msg.hash for msg in mensajes} - + total_procesados = 0 total_nuevos = 0 mensajes_duplicados = 0 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 for msg in nuevos_mensajes: if msg.hash not in mensajes_hash: @@ -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__': - main() \ No newline at end of file + +if __name__ == "__main__": + main() diff --git a/backend/script_groups/example_group/esquema_group.json b/backend/script_groups/example_group/esquema_group.json new file mode 100644 index 0000000..1c9e43a --- /dev/null +++ b/backend/script_groups/example_group/esquema_group.json @@ -0,0 +1,4 @@ +{ + "type": "object", + "properties": {} +} \ No newline at end of file diff --git a/backend/script_groups/example_group/esquema_work.json b/backend/script_groups/example_group/esquema_work.json new file mode 100644 index 0000000..1c9e43a --- /dev/null +++ b/backend/script_groups/example_group/esquema_work.json @@ -0,0 +1,4 @@ +{ + "type": "object", + "properties": {} +} \ No newline at end of file diff --git a/config_manager.py b/config_manager.py index ab5fcc0..f776713 100644 --- a/config_manager.py +++ b/config_manager.py @@ -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.""" diff --git a/data/log.txt b/data/log.txt index 2fdfa8f..dbff567 100644 --- a/data/log.txt +++ b/data/log.txt @@ -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 diff --git a/static/js/scripts.js b/static/js/scripts.js index b73a144..3520199 100644 --- a/static/js/scripts.js +++ b/static/js/scripts.js @@ -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 = `
Error cargando el esquema.
'; @@ -163,6 +192,18 @@ function generateInputField(def, key, value, level) { switch (def.type) { case 'string': + if (def.format === 'directory') { + return `