Corregido problema de eschemas en el mismo lugar
This commit is contained in:
parent
6fca251249
commit
930e578cec
|
@ -1,27 +0,0 @@
|
|||
# Python cache files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# Environment directories
|
||||
venv/
|
||||
env/
|
||||
.env/
|
||||
|
||||
# IDE configurations
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Logs and data files
|
||||
data/log.txt
|
||||
data/data.json
|
||||
|
||||
# Allow script groups and their configurations
|
||||
!backend/script_groups/
|
||||
!backend/script_groups/*/
|
||||
!backend/script_groups/*/*.py
|
||||
!backend/script_groups/*/schema.json
|
||||
!backend/script_groups/*/esquema.json
|
||||
!backend/script_groups/*/description.json
|
||||
|
||||
# But ignore working directory configurations
|
||||
backend/script_groups/*/work_dir.json
|
Binary file not shown.
6
app.py
6
app.py
|
@ -1,10 +1,12 @@
|
|||
from flask import Flask, render_template, request, jsonify
|
||||
from flask import Flask, render_template, request, jsonify, url_for
|
||||
from flask_sock import Sock
|
||||
from config_manager import ConfigurationManager
|
||||
import os
|
||||
import json # Added import
|
||||
|
||||
app = Flask(__name__)
|
||||
app = Flask(
|
||||
__name__, static_url_path="", static_folder="static", template_folder="templates"
|
||||
)
|
||||
sock = Sock(app)
|
||||
config_manager = ConfigurationManager()
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"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"
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,171 @@
|
|||
{
|
||||
"__documentation": {
|
||||
"__format": "Las reglas siguen el siguiente formato:",
|
||||
"pattern": "Patrón a buscar - puede ser texto o regex",
|
||||
"replacement": "Texto que reemplazará al patrón (puede estar vacío)",
|
||||
"action": "Tipo de acción: replace, remove_line, remove_block, add_before, add_after",
|
||||
"type": "Cómo interpretar el patrón: string, regex, left, right, substring",
|
||||
"priority": "Orden de ejecución (menor número = mayor prioridad)"
|
||||
},
|
||||
"__examples": {
|
||||
"replace": "Reemplaza texto: reemplaza cada coincidencia por el replacement",
|
||||
"remove_line": "Elimina línea: elimina la línea completa si encuentra el patrón",
|
||||
"remove_block": "Elimina bloque: elimina desde el inicio hasta el fin del patrón con .....",
|
||||
"add_before": "Agrega antes: inserta el replacement antes de la línea con el patrón",
|
||||
"add_after": "Agrega después: inserta el replacement después de la línea con el patrón"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"__comment": "Reemplaza non-breaking space por espacio normal",
|
||||
"pattern": "\u00a0",
|
||||
"replacement": " ",
|
||||
"action": "replace",
|
||||
"type": "string",
|
||||
"priority": 1
|
||||
},
|
||||
{
|
||||
"__comment": "Elimina marcador de mensaje original",
|
||||
"pattern": "--- Messaggio originale ---",
|
||||
"replacement": "***",
|
||||
"action": "remove_line",
|
||||
"type": "substring",
|
||||
"priority": 2
|
||||
},
|
||||
{
|
||||
"__comment": "Elimina firma de dispositivo móvil",
|
||||
"pattern": "(?m)^Sent from my.*$",
|
||||
"replacement": "",
|
||||
"action": "remove_line",
|
||||
"type": "regex",
|
||||
"priority": 2
|
||||
},
|
||||
{
|
||||
"__comment": "Elimina aviso medioambiental",
|
||||
"pattern": "(?m)^Please take care of the environment.*$",
|
||||
"replacement": "",
|
||||
"action": "remove_line",
|
||||
"type": "regex",
|
||||
"priority": 2
|
||||
},
|
||||
{
|
||||
"__comment": "Elimina aviso de mensaje automático",
|
||||
"pattern": "(?m)^This message is from an.*$",
|
||||
"replacement": "",
|
||||
"action": "remove_line",
|
||||
"type": "regex",
|
||||
"priority": 2
|
||||
},
|
||||
{
|
||||
"__comment": "Elimina aviso de confidencialidad en italiano",
|
||||
"pattern": "eventuali allegati sono confidenziali",
|
||||
"replacement": "",
|
||||
"action": "remove_line",
|
||||
"type": "substring",
|
||||
"priority": 2
|
||||
},
|
||||
{
|
||||
"__comment": "Elimina aviso de confidencialidad en inglés",
|
||||
"pattern": "any attachments are confidential",
|
||||
"replacement": "",
|
||||
"action": "remove_line",
|
||||
"type": "substring",
|
||||
"priority": 2
|
||||
},
|
||||
{
|
||||
"__comment": "Elimina solicitud de LinkedIn",
|
||||
"pattern": "Please sign up on our Linkedin",
|
||||
"replacement": "",
|
||||
"action": "remove_line",
|
||||
"type": "left",
|
||||
"priority": 2
|
||||
},
|
||||
{
|
||||
"__comment": "Elimina aviso de no compartir contenido",
|
||||
"pattern": "di non copiare o condividere i contenuti con nessuno",
|
||||
"replacement": "",
|
||||
"action": "remove_line",
|
||||
"type": "substring",
|
||||
"priority": 2
|
||||
},
|
||||
{
|
||||
"__comment": "Elimina líneas de email individual",
|
||||
"pattern": "(?m)^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
|
||||
"replacement": "",
|
||||
"action": "remove_line",
|
||||
"type": "regex",
|
||||
"priority": 2
|
||||
},
|
||||
{
|
||||
"__comment": "Elimina líneas con múltiples emails",
|
||||
"pattern": "(?m)(?:^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}\\s*(?:;\\s*)?$|^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}\\s*;\\s*[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}.*$)",
|
||||
"replacement": "",
|
||||
"action": "remove_line",
|
||||
"type": "regex",
|
||||
"priority": 2
|
||||
},
|
||||
{
|
||||
"__comment": "Elimina línea de teléfono",
|
||||
"pattern": "Phone:",
|
||||
"replacement": "",
|
||||
"action": "remove_line",
|
||||
"type": "left",
|
||||
"priority": 2
|
||||
},
|
||||
{
|
||||
"__comment": "Elimina línea de móvil",
|
||||
"pattern": "Mobile:",
|
||||
"replacement": "",
|
||||
"action": "remove_line",
|
||||
"type": "left",
|
||||
"priority": 2
|
||||
},
|
||||
{
|
||||
"__comment": "Elimina línea de CC",
|
||||
"pattern": "Cc:",
|
||||
"replacement": "",
|
||||
"action": "remove_line",
|
||||
"type": "left",
|
||||
"priority": 2
|
||||
},
|
||||
{
|
||||
"__comment": "Elimina línea de destinatario (italiano)",
|
||||
"pattern": "A:",
|
||||
"replacement": "",
|
||||
"action": "remove_line",
|
||||
"type": "left",
|
||||
"priority": 2
|
||||
},
|
||||
{
|
||||
"__comment": "Elimina línea de destinatario",
|
||||
"pattern": "To:",
|
||||
"replacement": "",
|
||||
"action": "remove_line",
|
||||
"type": "left",
|
||||
"priority": 2
|
||||
},
|
||||
{
|
||||
"__comment": "Agrega separador antes del asunto",
|
||||
"pattern": "Subject: ",
|
||||
"replacement": "***",
|
||||
"action": "add_before",
|
||||
"type": "left",
|
||||
"priority": 3
|
||||
},
|
||||
{
|
||||
"__comment": "Elimina firma corporativa",
|
||||
"pattern": "Strada Isolanda.....Website:www.vetromeccanica.it",
|
||||
"replacement": "",
|
||||
"action": "remove_block",
|
||||
"type": "string",
|
||||
"priority": 4
|
||||
},
|
||||
{
|
||||
"__comment": "Elimina aviso legal largo",
|
||||
"pattern": "IMPORTANT NOTICE: This message may.....without retaining any copy",
|
||||
"replacement": "",
|
||||
"action": "remove_block",
|
||||
"type": "string",
|
||||
"priority": 4
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
# config/config.py
|
||||
import json
|
||||
import os
|
||||
|
||||
class Config:
|
||||
def __init__(self, config_file='config.json'):
|
||||
self.config_file = config_file
|
||||
self.config = self._load_config()
|
||||
|
||||
def _load_config(self):
|
||||
if not os.path.exists(self.config_file):
|
||||
default_config = {
|
||||
'input_dir': '.',
|
||||
'output_dir': '.',
|
||||
'cronologia_file': 'cronologia.md',
|
||||
'attachments_dir': 'adjuntos'
|
||||
}
|
||||
self._save_config(default_config)
|
||||
return default_config
|
||||
|
||||
with open(self.config_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
def _save_config(self, config):
|
||||
with open(self.config_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, indent=4)
|
||||
|
||||
def get_input_dir(self):
|
||||
return self.config.get('input_dir', '.')
|
||||
|
||||
def get_output_dir(self):
|
||||
return self.config.get('output_dir', '.')
|
||||
|
||||
def get_cronologia_file(self):
|
||||
return os.path.join(
|
||||
self.get_output_dir(),
|
||||
self.config.get('cronologia_file', 'cronologia.md')
|
||||
)
|
||||
|
||||
def get_attachments_dir(self):
|
||||
return os.path.join(
|
||||
self.get_output_dir(),
|
||||
self.config.get('attachments_dir', 'adjuntos')
|
||||
)
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"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"
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "Desempaquetado de Emails EML",
|
||||
"description": "This script processes email files (.eml) into a chronological narrative in Markdown format, optimized for processing with Large Language Models (LLMs). It extracts essential information from emails while removing unnecessary metadata, creating a clean, temporal narrative that can be easily analyzed. ",
|
||||
"version": "1.0",
|
||||
"author": "Unknown"
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"attachments_dir": {
|
||||
"type": "string",
|
||||
"title": "Directorio de adjuntos",
|
||||
"description": "adjuntos"
|
||||
},
|
||||
"cronologia_file": {
|
||||
"type": "string",
|
||||
"title": "Nombre del archivo de cronologia.md",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"output_dir": {
|
||||
"type": "string",
|
||||
"title": "Directorio de destino de la cronologia",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,145 @@
|
|||
# models/mensaje_email.py
|
||||
import re
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
from email.utils import parseaddr, parsedate_to_datetime
|
||||
|
||||
class MensajeEmail:
|
||||
def __init__(self, remitente, fecha, contenido, subject=None, adjuntos=None):
|
||||
self.remitente = self._estandarizar_remitente(remitente)
|
||||
self.fecha = self._estandarizar_fecha(fecha)
|
||||
self.subject = subject if subject else 'Sin Asunto'
|
||||
self.contenido = self._limpiar_contenido(contenido)
|
||||
self.adjuntos = adjuntos if adjuntos else []
|
||||
self.hash = self._generar_hash()
|
||||
|
||||
def _formatear_subject_para_link(self, subject):
|
||||
"""
|
||||
Formatea el subject para usarlo como ancla en links de Obsidian
|
||||
Remueve caracteres especiales y espacios múltiples
|
||||
"""
|
||||
if not subject:
|
||||
return "Sin-Asunto"
|
||||
# Eliminar caracteres especiales y reemplazar espacios con guiones
|
||||
formatted = re.sub(r'[^\w\s-]', '', subject)
|
||||
formatted = re.sub(r'\s+', '-', formatted.strip())
|
||||
return formatted
|
||||
|
||||
def _limpiar_contenido(self, contenido):
|
||||
if not contenido:
|
||||
return ""
|
||||
|
||||
# Eliminar líneas de metadatos
|
||||
lines = contenido.split('\n')
|
||||
cleaned_lines = []
|
||||
|
||||
for line in lines:
|
||||
# Skip metadata lines
|
||||
if line.strip().startswith(('Da: ', 'Inviato: ', 'A: ', 'From: ', 'Sent: ', 'To: ')) or line.strip().startswith('Oggetto: '):
|
||||
continue
|
||||
# Limpiar espacios múltiples dentro de cada línea, pero mantener la línea completa
|
||||
cleaned_line = re.sub(r' +', ' ', line)
|
||||
cleaned_lines.append(cleaned_line)
|
||||
|
||||
# Unir las líneas preservando los saltos de línea
|
||||
text = '\n'.join(cleaned_lines)
|
||||
|
||||
# Limpiar la combinación específica de CRLF+NBSP+CRLF
|
||||
text = re.sub(r'\r?\n\xa0\r?\n', '\n', text)
|
||||
|
||||
# Reemplazar CRLF por LF
|
||||
text = text.replace('\r\n', '\n')
|
||||
|
||||
# Reemplazar CR por LF
|
||||
text = text.replace('\r', '\n')
|
||||
|
||||
# Reemplazar 3 o más saltos de línea por dos
|
||||
text = re.sub(r'\n{3,}', '\n\n', text)
|
||||
|
||||
# Eliminar espacios al inicio y final del texto completo
|
||||
return text.strip()
|
||||
|
||||
def to_markdown(self):
|
||||
# Hash con caracteres no título
|
||||
hash_line = f"+ {self.hash}\n\n"
|
||||
|
||||
# Subject como título
|
||||
subject_line = f"### {self.subject if self.subject else 'Sin Asunto'}\n\n"
|
||||
|
||||
# Fecha en formato legible
|
||||
fecha_formato = self.fecha.strftime('%d-%m-%Y')
|
||||
fecha_line = f"- {fecha_formato}\n\n"
|
||||
|
||||
# Contenido del mensaje
|
||||
md = f"{hash_line}{subject_line}{fecha_line}"
|
||||
md += self.contenido + "\n\n"
|
||||
|
||||
# Adjuntos si existen
|
||||
if self.adjuntos:
|
||||
md += "### Adjuntos\n"
|
||||
for adj in self.adjuntos:
|
||||
md += f"- [[{adj}]]\n"
|
||||
md += "---\n\n"
|
||||
return md
|
||||
|
||||
def get_index_entry(self):
|
||||
"""
|
||||
Genera una entrada de lista para el índice
|
||||
"""
|
||||
fecha_formato = self.fecha.strftime('%d-%m-%Y')
|
||||
subject_link = self._formatear_subject_para_link(self.subject)
|
||||
return f"- {fecha_formato} - {self.remitente} - [[cronologia#{self.subject}|{subject_link}]]"
|
||||
|
||||
def _estandarizar_remitente(self, remitente):
|
||||
if 'Da:' in remitente:
|
||||
remitente = remitente.split('Da:')[1].split('Inviato:')[0]
|
||||
elif 'From:' in remitente:
|
||||
remitente = remitente.split('From:')[1].split('Sent:')[0]
|
||||
|
||||
nombre, email = parseaddr(remitente)
|
||||
if not nombre and email:
|
||||
nombre = email.split('@')[0]
|
||||
elif not nombre and not email:
|
||||
nombre_match = re.search(r'([A-Za-z\s]+)\s*<', remitente)
|
||||
if nombre_match:
|
||||
nombre = nombre_match.group(1)
|
||||
else:
|
||||
return "Remitente Desconocido"
|
||||
|
||||
nombre = re.sub(r'[<>:"/\\|?*]', '', nombre.strip())
|
||||
nombre = nombre.encode('ascii', 'ignore').decode('ascii')
|
||||
return nombre
|
||||
|
||||
def _estandarizar_fecha(self, fecha):
|
||||
if isinstance(fecha, str):
|
||||
try:
|
||||
return parsedate_to_datetime(fecha)
|
||||
except:
|
||||
return datetime.now()
|
||||
return fecha
|
||||
|
||||
def _generar_hash(self):
|
||||
"""
|
||||
Genera un hash único para el mensaje basado en una combinación de campos
|
||||
que identifican únicamente el mensaje
|
||||
"""
|
||||
# Limpiar y normalizar el contenido para el hash
|
||||
# Para el hash, sí normalizamos completamente los espacios
|
||||
contenido_hash = re.sub(r'\s+', ' ', self.contenido).strip()
|
||||
|
||||
# Normalizar el subject
|
||||
subject_normalizado = re.sub(r'\s+', ' ', self.subject if self.subject else '').strip()
|
||||
|
||||
# Crear una cadena con los elementos clave del mensaje
|
||||
elementos_hash = [
|
||||
self.remitente.strip(),
|
||||
self.fecha.strftime('%Y%m%d%H%M'), # Solo hasta minutos para permitir pequeñas variaciones
|
||||
subject_normalizado,
|
||||
contenido_hash[:500] # Usar solo los primeros 500 caracteres del contenido normalizado
|
||||
]
|
||||
|
||||
# Unir todos los elementos con un separador único
|
||||
texto_hash = '|'.join(elementos_hash)
|
||||
|
||||
# Generar el hash
|
||||
return hashlib.md5(texto_hash.encode()).hexdigest()
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,33 @@
|
|||
# utils/attachment_handler.py
|
||||
import os
|
||||
import hashlib
|
||||
import re
|
||||
|
||||
def guardar_adjunto(parte, dir_adjuntos):
|
||||
nombre = parte.get_filename()
|
||||
if not nombre:
|
||||
return None
|
||||
|
||||
nombre = re.sub(r'[<>:"/\\|?*]', '_', nombre)
|
||||
ruta = os.path.join(dir_adjuntos, nombre)
|
||||
|
||||
if os.path.exists(ruta):
|
||||
contenido_nuevo = parte.get_payload(decode=True)
|
||||
hash_nuevo = hashlib.md5(contenido_nuevo).hexdigest()
|
||||
|
||||
with open(ruta, 'rb') as f:
|
||||
hash_existente = hashlib.md5(f.read()).hexdigest()
|
||||
|
||||
if hash_nuevo == hash_existente:
|
||||
return ruta
|
||||
|
||||
base, ext = os.path.splitext(nombre)
|
||||
i = 1
|
||||
while os.path.exists(ruta):
|
||||
ruta = os.path.join(dir_adjuntos, f"{base}_{i}{ext}")
|
||||
i += 1
|
||||
|
||||
with open(ruta, 'wb') as f:
|
||||
f.write(parte.get_payload(decode=True))
|
||||
|
||||
return ruta
|
|
@ -0,0 +1,225 @@
|
|||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
from enum import Enum
|
||||
|
||||
class PatternType(Enum):
|
||||
REGEX = "regex"
|
||||
STRING = "string"
|
||||
LEFT = "left"
|
||||
RIGHT = "right"
|
||||
SUBSTRING = "substring"
|
||||
|
||||
class BeautifyProcessor:
|
||||
def __init__(self, rules_file):
|
||||
self.rules_by_priority = self._load_rules(rules_file)
|
||||
|
||||
def _load_rules(self, rules_file):
|
||||
rules_by_priority = defaultdict(list)
|
||||
|
||||
try:
|
||||
with open(rules_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
if not isinstance(data, dict) or 'rules' not in data:
|
||||
raise ValueError("El archivo JSON debe contener un objeto con una clave 'rules'")
|
||||
|
||||
for rule in data['rules']:
|
||||
try:
|
||||
pattern = rule['pattern']
|
||||
replacement = rule['replacement']
|
||||
action = rule['action']
|
||||
pattern_type = PatternType(rule.get('type', 'string'))
|
||||
priority = int(rule.get('priority', 999))
|
||||
|
||||
# Para remove_block, convertir el patrón con ..... a una regex
|
||||
if action == "remove_block":
|
||||
pattern = self._convert_block_pattern_to_regex(pattern)
|
||||
pattern_type = PatternType.REGEX
|
||||
elif pattern_type == PatternType.REGEX:
|
||||
pattern = re.compile(pattern)
|
||||
|
||||
rules_by_priority[priority].append((pattern, replacement, action, pattern_type))
|
||||
|
||||
except KeyError as e:
|
||||
print(f"Error en regla: falta campo requerido {e}")
|
||||
continue
|
||||
except ValueError as e:
|
||||
print(f"Error en regla: tipo de patrón inválido {rule.get('type')}")
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"Error procesando regla: {e}")
|
||||
continue
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error decodificando JSON: {e}")
|
||||
except Exception as e:
|
||||
print(f"Error cargando reglas: {e}")
|
||||
|
||||
return rules_by_priority
|
||||
|
||||
def _convert_block_pattern_to_regex(self, pattern):
|
||||
"""
|
||||
Convierte un patrón de bloque con ..... en una expresión regular.
|
||||
Primero maneja el comodín ..... y luego escapa el resto de caracteres especiales.
|
||||
"""
|
||||
# Reemplazar temporalmente los ..... con un marcador único
|
||||
marker = "__BLOCK_MARKER__"
|
||||
pattern = pattern.replace(".....", marker)
|
||||
|
||||
# Escapar caracteres especiales
|
||||
pattern = re.escape(pattern)
|
||||
|
||||
# Restaurar el marcador con el patrón .*?
|
||||
pattern = pattern.replace(marker, ".*?")
|
||||
|
||||
return re.compile(f'(?s){pattern}')
|
||||
|
||||
def _process_remove_block(self, text, pattern):
|
||||
result = text
|
||||
matches = list(pattern.finditer(result))
|
||||
|
||||
for match in reversed(matches):
|
||||
start, end = match.span()
|
||||
|
||||
line_start = result.rfind('\n', 0, start) + 1
|
||||
if line_start == 0:
|
||||
line_start = 0
|
||||
|
||||
line_end = result.find('\n', end)
|
||||
if line_end == -1:
|
||||
line_end = len(result)
|
||||
else:
|
||||
line_end += 1
|
||||
|
||||
while line_start > 0 and result[line_start-1:line_start] == '\n' and \
|
||||
(line_start == 1 or result[line_start-2:line_start-1] == '\n'):
|
||||
line_start -= 1
|
||||
|
||||
while line_end < len(result) and result[line_end-1:line_end] == '\n' and \
|
||||
(line_end == len(result)-1 or result[line_end:line_end+1] == '\n'):
|
||||
line_end += 1
|
||||
|
||||
result = result[:line_start] + result[line_end:]
|
||||
|
||||
return result
|
||||
|
||||
def _line_matches(self, line, pattern, pattern_type):
|
||||
line = line.strip()
|
||||
if pattern_type == PatternType.REGEX:
|
||||
return bool(pattern.search(line))
|
||||
elif pattern_type == PatternType.LEFT:
|
||||
return line.startswith(pattern)
|
||||
elif pattern_type == PatternType.RIGHT:
|
||||
return line.endswith(pattern)
|
||||
elif pattern_type == PatternType.SUBSTRING:
|
||||
return pattern in line
|
||||
elif pattern_type == PatternType.STRING:
|
||||
return line == pattern
|
||||
return False
|
||||
|
||||
def _apply_replace(self, text, pattern, replacement, pattern_type):
|
||||
if pattern_type == PatternType.REGEX:
|
||||
return pattern.sub(replacement, text)
|
||||
elif pattern_type == PatternType.STRING:
|
||||
return text.replace(pattern, replacement)
|
||||
elif pattern_type == PatternType.SUBSTRING:
|
||||
return text.replace(pattern, replacement)
|
||||
elif pattern_type == PatternType.LEFT:
|
||||
lines = text.splitlines()
|
||||
result_lines = []
|
||||
for line in lines:
|
||||
if line.strip().startswith(pattern):
|
||||
result_lines.append(line.replace(pattern, replacement, 1))
|
||||
else:
|
||||
result_lines.append(line)
|
||||
return '\n'.join(result_lines)
|
||||
elif pattern_type == PatternType.RIGHT:
|
||||
lines = text.splitlines()
|
||||
result_lines = []
|
||||
for line in lines:
|
||||
if line.strip().endswith(pattern):
|
||||
result_lines.append(line[:line.rindex(pattern)] + replacement + line[line.rindex(pattern) + len(pattern):])
|
||||
else:
|
||||
result_lines.append(line)
|
||||
return '\n'.join(result_lines)
|
||||
return text
|
||||
|
||||
def process_text(self, text):
|
||||
if not text:
|
||||
return text
|
||||
|
||||
result = text
|
||||
for priority in sorted(self.rules_by_priority.keys()):
|
||||
rules = self.rules_by_priority[priority]
|
||||
print(f"Aplicando reglas de prioridad {priority}")
|
||||
|
||||
for pattern, replacement, action, pattern_type in rules:
|
||||
try:
|
||||
if action == "remove_block":
|
||||
result = self._process_remove_block(result, pattern)
|
||||
elif action == "replace":
|
||||
result = self._apply_replace(result, pattern, replacement, pattern_type)
|
||||
elif action == "remove_line":
|
||||
result = self._process_remove_line(result, pattern, pattern_type)
|
||||
elif action in ["add_before", "add_after"]:
|
||||
result = self._process_line_additions(result, pattern, replacement, action, pattern_type)
|
||||
except Exception as e:
|
||||
print(f"Error aplicando regla {pattern}: {e}")
|
||||
continue
|
||||
|
||||
return result
|
||||
|
||||
def process_file(self, input_file, output_file=None):
|
||||
try:
|
||||
with open(input_file, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
processed_content = self.process_text(content)
|
||||
|
||||
output = output_file or input_file
|
||||
with open(output, 'w', encoding='utf-8') as f:
|
||||
f.write(processed_content)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error procesando archivo {input_file}: {e}")
|
||||
|
||||
def _process_remove_line(self, text, pattern, pattern_type):
|
||||
lines = text.splitlines()
|
||||
result_lines = []
|
||||
skip_next_empty = False
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
should_remove = self._line_matches(line, pattern, pattern_type)
|
||||
|
||||
if should_remove:
|
||||
if i < len(lines) - 1 and not lines[i + 1].strip():
|
||||
skip_next_empty = True
|
||||
continue
|
||||
|
||||
if skip_next_empty and not line.strip():
|
||||
skip_next_empty = False
|
||||
continue
|
||||
|
||||
result_lines.append(line)
|
||||
skip_next_empty = False
|
||||
|
||||
return '\n'.join(result_lines)
|
||||
|
||||
def _process_line_additions(self, text, pattern, replacement, action, pattern_type):
|
||||
lines = text.splitlines()
|
||||
result_lines = []
|
||||
|
||||
for line in lines:
|
||||
if self._line_matches(line, pattern, pattern_type):
|
||||
if action == "add_before":
|
||||
result_lines.append(replacement)
|
||||
result_lines.append(line)
|
||||
else: # add_after
|
||||
result_lines.append(line)
|
||||
result_lines.append(replacement)
|
||||
else:
|
||||
result_lines.append(line)
|
||||
|
||||
return '\n'.join(result_lines)
|
|
@ -0,0 +1,295 @@
|
|||
# utils/email_parser.py
|
||||
import email
|
||||
from email import policy
|
||||
from email.parser import BytesParser
|
||||
from datetime import datetime
|
||||
import re
|
||||
from pathlib import Path
|
||||
from bs4 import BeautifulSoup
|
||||
from email.utils import parsedate_to_datetime
|
||||
from models.mensaje_email import MensajeEmail
|
||||
from utils.attachment_handler import guardar_adjunto
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
def _get_payload_safely(parte):
|
||||
"""
|
||||
Obtiene el payload de una parte del email de forma segura
|
||||
"""
|
||||
try:
|
||||
if parte.is_multipart():
|
||||
return None
|
||||
payload = parte.get_payload(decode=True)
|
||||
if payload is None:
|
||||
return None
|
||||
charset = parte.get_content_charset() or 'utf-8'
|
||||
return payload.decode(charset, errors='ignore')
|
||||
except Exception as e:
|
||||
print(f"Error getting payload: {str(e)}")
|
||||
return None
|
||||
|
||||
def _extract_subject_from_text(text):
|
||||
"""
|
||||
Extrae el asunto de un texto dados diferentes formatos de cabecera
|
||||
"""
|
||||
subject_headers = {
|
||||
'Oggetto: ': 9, # Italian
|
||||
'Subject: ': 9, # English
|
||||
'Asunto: ': 8, # Spanish
|
||||
'Sujet: ': 7, # French
|
||||
'Betreff: ': 9 # German
|
||||
}
|
||||
|
||||
for line in text.split('\n'):
|
||||
line = line.strip()
|
||||
for header, offset in subject_headers.items():
|
||||
if line.startswith(header):
|
||||
return line[offset:].strip()
|
||||
return None
|
||||
|
||||
def _should_skip_line(line):
|
||||
"""
|
||||
Determina si una línea debe ser omitida por ser una cabecera de email
|
||||
"""
|
||||
headers_to_skip = [
|
||||
'Da: ', 'Inviato: ', 'A: ', # Italian
|
||||
'From: ', 'Sent: ', 'To: ', # English
|
||||
'De: ', 'Enviado: ', 'Para: ', # Spanish
|
||||
'Von: ', 'Gesendet: ', 'An: ', # German
|
||||
'De : ', 'Envoyé : ', 'À : ' # French
|
||||
]
|
||||
return any(line.strip().startswith(header) for header in headers_to_skip)
|
||||
|
||||
def _html_a_markdown(html):
|
||||
"""
|
||||
Convierte contenido HTML a texto markdown, extrayendo el asunto si está presente
|
||||
"""
|
||||
if html is None:
|
||||
return (None, "")
|
||||
|
||||
try:
|
||||
# Limpieza básica
|
||||
html = html.replace('\xa0', ' ') # NBSP a espacio normal
|
||||
html = html.replace('\r\n', '\n') # CRLF a LF
|
||||
html = html.replace('\r', '\n') # CR a LF
|
||||
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
|
||||
# Procesar tablas
|
||||
for table in soup.find_all('table'):
|
||||
try:
|
||||
rows = table.find_all('tr')
|
||||
if not rows:
|
||||
continue
|
||||
|
||||
markdown_table = []
|
||||
max_widths = []
|
||||
|
||||
# Calcular anchos máximos
|
||||
for row in rows:
|
||||
cells = row.find_all(['th', 'td'])
|
||||
while len(max_widths) < len(cells):
|
||||
max_widths.append(0)
|
||||
for i, cell in enumerate(cells):
|
||||
cell_text = cell.get_text().strip()
|
||||
max_widths[i] = max(max_widths[i], len(cell_text))
|
||||
|
||||
# Construir tabla markdown
|
||||
if max_widths: # Solo si tenemos celdas válidas
|
||||
header_row = rows[0].find_all(['th', 'td'])
|
||||
header = '| ' + ' | '.join(cell.get_text().strip().ljust(max_widths[i])
|
||||
for i, cell in enumerate(header_row)) + ' |'
|
||||
separator = '|' + '|'.join('-' * (width + 2) for width in max_widths) + '|'
|
||||
|
||||
markdown_table.append(header)
|
||||
markdown_table.append(separator)
|
||||
|
||||
for row in rows[1:]:
|
||||
cells = row.find_all(['td', 'th'])
|
||||
row_text = '| ' + ' | '.join(cell.get_text().strip().ljust(max_widths[i])
|
||||
for i, cell in enumerate(cells)) + ' |'
|
||||
markdown_table.append(row_text)
|
||||
|
||||
table.replace_with(soup.new_string('\n' + '\n'.join(markdown_table)))
|
||||
except Exception as e:
|
||||
print(f"Error procesando tabla: {str(e)}")
|
||||
continue
|
||||
|
||||
# Procesar saltos de línea
|
||||
for br in soup.find_all('br'):
|
||||
br.replace_with('\n')
|
||||
|
||||
# Obtener texto limpio
|
||||
text = soup.get_text()
|
||||
|
||||
# Procesar líneas
|
||||
cleaned_lines = []
|
||||
subject = None
|
||||
|
||||
for line in text.split('\n'):
|
||||
if not subject:
|
||||
subject = _extract_subject_from_text(line)
|
||||
|
||||
if not _should_skip_line(line):
|
||||
cleaned_lines.append(line)
|
||||
|
||||
final_text = '\n'.join(cleaned_lines).strip()
|
||||
return (subject, final_text)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error en html_a_markdown: {str(e)}")
|
||||
return (None, html if html else "")
|
||||
|
||||
def _procesar_email_adjunto(parte, dir_adjuntos):
|
||||
"""
|
||||
Procesa un email que viene como adjunto dentro de otro email.
|
||||
"""
|
||||
try:
|
||||
mensajes = []
|
||||
if parte.is_multipart():
|
||||
# Si es multipart, procesar cada subparte
|
||||
for subparte in parte.walk():
|
||||
if subparte.get_content_type() == "message/rfc822":
|
||||
# Si es un mensaje RFC822, obtener el payload como lista
|
||||
payload = subparte.get_payload()
|
||||
if isinstance(payload, list):
|
||||
for msg in payload:
|
||||
mensajes.extend(procesar_eml_interno(msg, dir_adjuntos))
|
||||
elif isinstance(payload, email.message.Message):
|
||||
mensajes.extend(procesar_eml_interno(payload, dir_adjuntos))
|
||||
else:
|
||||
# Si no es multipart, intentar procesar como mensaje único
|
||||
payload = parte.get_payload()
|
||||
if isinstance(payload, list):
|
||||
for msg in payload:
|
||||
mensajes.extend(procesar_eml_interno(msg, dir_adjuntos))
|
||||
elif isinstance(payload, email.message.Message):
|
||||
mensajes.extend(procesar_eml_interno(payload, dir_adjuntos))
|
||||
|
||||
return mensajes
|
||||
except Exception as e:
|
||||
print(f"Error procesando email adjunto: {str(e)}")
|
||||
return []
|
||||
|
||||
def procesar_eml(ruta_archivo, dir_adjuntos):
|
||||
"""
|
||||
Punto de entrada principal para procesar archivos .eml
|
||||
"""
|
||||
try:
|
||||
with open(ruta_archivo, 'rb') as eml:
|
||||
mensaje = BytesParser(policy=policy.default).parse(eml)
|
||||
return procesar_eml_interno(mensaje, dir_adjuntos)
|
||||
except Exception as e:
|
||||
print(f"Error al abrir el archivo {ruta_archivo}: {str(e)}")
|
||||
return []
|
||||
|
||||
def procesar_eml_interno(mensaje, dir_adjuntos):
|
||||
"""
|
||||
Procesa un mensaje de email, ya sea desde archivo o adjunto
|
||||
"""
|
||||
mensajes = []
|
||||
|
||||
try:
|
||||
remitente = mensaje.get('from', '')
|
||||
fecha_str = mensaje.get('date', '')
|
||||
fecha = _parsear_fecha(fecha_str)
|
||||
|
||||
# Get subject from email headers first
|
||||
subject = mensaje.get('subject', '')
|
||||
if subject:
|
||||
# Try to decode if it's encoded
|
||||
subject = str(email.header.make_header(email.header.decode_header(subject)))
|
||||
|
||||
contenido = ""
|
||||
adjuntos = []
|
||||
tiene_html = False
|
||||
|
||||
# First pass: check for HTML content
|
||||
if mensaje.is_multipart():
|
||||
for parte in mensaje.walk():
|
||||
if parte.get_content_type() == "text/html":
|
||||
tiene_html = True
|
||||
break
|
||||
else:
|
||||
tiene_html = mensaje.get_content_type() == "text/html"
|
||||
|
||||
# Second pass: process content and attachments
|
||||
if mensaje.is_multipart():
|
||||
for parte in mensaje.walk():
|
||||
content_type = parte.get_content_type()
|
||||
|
||||
try:
|
||||
if content_type == "text/html":
|
||||
html_content = _get_payload_safely(parte)
|
||||
if html_content:
|
||||
part_subject, text = _html_a_markdown(html_content)
|
||||
if not subject and part_subject:
|
||||
subject = part_subject
|
||||
if text:
|
||||
contenido = text
|
||||
elif content_type == "text/plain" and not tiene_html:
|
||||
text = _get_payload_safely(parte)
|
||||
if text:
|
||||
contenido = text
|
||||
elif content_type == "message/rfc822":
|
||||
# Procesar email adjunto
|
||||
mensajes_adjuntos = _procesar_email_adjunto(parte, dir_adjuntos)
|
||||
mensajes.extend(mensajes_adjuntos)
|
||||
elif parte.get_content_disposition() == 'attachment':
|
||||
nombre = parte.get_filename()
|
||||
if nombre and nombre.lower().endswith('.eml'):
|
||||
# Si es un archivo .eml adjunto
|
||||
mensajes_adjuntos = _procesar_email_adjunto(parte, dir_adjuntos)
|
||||
mensajes.extend(mensajes_adjuntos)
|
||||
else:
|
||||
# Otros tipos de adjuntos
|
||||
ruta_adjunto = guardar_adjunto(parte, dir_adjuntos)
|
||||
if ruta_adjunto:
|
||||
adjuntos.append(Path(ruta_adjunto).name)
|
||||
except Exception as e:
|
||||
print(f"Error procesando parte del mensaje: {str(e)}")
|
||||
continue
|
||||
else:
|
||||
if mensaje.get_content_type() == "text/html":
|
||||
html_content = _get_payload_safely(mensaje)
|
||||
if html_content:
|
||||
part_subject, contenido = _html_a_markdown(html_content)
|
||||
if not subject and part_subject:
|
||||
subject = part_subject
|
||||
else:
|
||||
contenido = _get_payload_safely(mensaje) or ""
|
||||
|
||||
# Solo agregar el mensaje si tiene contenido útil
|
||||
if contenido or subject or adjuntos:
|
||||
mensajes.append(MensajeEmail(
|
||||
remitente=remitente,
|
||||
fecha=fecha,
|
||||
contenido=contenido,
|
||||
subject=subject,
|
||||
adjuntos=adjuntos
|
||||
))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error procesando mensaje: {str(e)}")
|
||||
|
||||
return mensajes
|
||||
|
||||
def _parsear_fecha(fecha_str):
|
||||
try:
|
||||
fecha = parsedate_to_datetime(fecha_str)
|
||||
return fecha.replace(tzinfo=None) # Remove timezone info
|
||||
except:
|
||||
try:
|
||||
fecha_match = re.search(r'venerd=EC (\d{1,2}) (\w+) (\d{4}) (\d{1,2}):(\d{2})', fecha_str)
|
||||
if fecha_match:
|
||||
dia, mes, año, hora, minuto = fecha_match.groups()
|
||||
meses_it = {
|
||||
'gennaio': 1, 'febbraio': 2, 'marzo': 3, 'aprile': 4,
|
||||
'maggio': 5, 'giugno': 6, 'luglio': 7, 'agosto': 8,
|
||||
'settembre': 9, 'ottobre': 10, 'novembre': 11, 'dicembre': 12
|
||||
}
|
||||
mes_num = meses_it.get(mes.lower(), 1)
|
||||
return datetime(int(año), mes_num, int(dia), int(hora), int(minuto))
|
||||
except:
|
||||
pass
|
||||
return datetime.now()
|
|
@ -0,0 +1,39 @@
|
|||
# utils/markdown_handler.py
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from models.mensaje_email import MensajeEmail
|
||||
|
||||
def cargar_cronologia_existente(archivo):
|
||||
mensajes = []
|
||||
if not os.path.exists(archivo):
|
||||
return mensajes
|
||||
|
||||
with open(archivo, 'r', encoding='utf-8') as f:
|
||||
contenido = f.read()
|
||||
|
||||
bloques = contenido.split('---\n\n')
|
||||
for bloque in bloques:
|
||||
if not bloque.strip():
|
||||
continue
|
||||
|
||||
match = re.match(r'## (\d{14})\|(.*?)\n\n(.*)', bloque.strip(), re.DOTALL)
|
||||
if match:
|
||||
fecha_str, remitente, contenido = match.groups()
|
||||
fecha = datetime.strptime(fecha_str, '%Y%m%d%H%M%S')
|
||||
|
||||
adjuntos = []
|
||||
if '### Adjuntos' in contenido:
|
||||
contenido_principal, lista_adjuntos = contenido.split('### Adjuntos')
|
||||
adjuntos = [adj.strip()[2:-2] for adj in lista_adjuntos.strip().split('\n')]
|
||||
contenido = contenido_principal.strip()
|
||||
|
||||
mensajes.append(MensajeEmail(
|
||||
remitente=remitente,
|
||||
fecha=fecha,
|
||||
contenido=contenido,
|
||||
adjuntos=adjuntos
|
||||
))
|
||||
|
||||
return mensajes
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"path": "C:/Trabajo/VM/40 - 93040 - HENKEL - NEXT2 Problem/Reporte/Emails"
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
"""
|
||||
Script para dessasemblar los emails y generar un archivo de texto con la cronología de los mensajes.
|
||||
"""
|
||||
|
||||
# main.py
|
||||
import os
|
||||
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
|
||||
|
||||
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()
|
||||
|
||||
# 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()}")
|
||||
|
||||
# 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)
|
||||
|
||||
# Check if input directory exists and has files
|
||||
input_path = Path(config.get_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'))
|
||||
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())
|
||||
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:
|
||||
# Aplicar beautify solo si el mensaje es nuevo
|
||||
msg.contenido = beautifier.process_text(msg.contenido)
|
||||
mensajes.append(msg)
|
||||
mensajes_hash.add(msg.hash)
|
||||
total_nuevos += 1
|
||||
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}")
|
||||
|
||||
# Ordenar mensajes de más reciente a más antiguo
|
||||
mensajes.sort(key=lambda x: x.fecha, reverse=True)
|
||||
|
||||
# Generar el índice
|
||||
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:
|
||||
# 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()
|
|
@ -1,3 +0,0 @@
|
|||
# Crear .gitkeep en cada directorio de script grupo
|
||||
mkdir -p backend/script_groups/example_group
|
||||
touch backend/script_groups/example_group/.gitkeep
|
|
@ -122,22 +122,13 @@ class ConfigurationManager:
|
|||
|
||||
# Determine schema path based on level
|
||||
if level == "1":
|
||||
path = os.path.join(self.data_path, "esquema.json")
|
||||
# Try esquema.json first, then schema.json if not found
|
||||
if not os.path.exists(path):
|
||||
path = os.path.join(self.data_path, "schema.json")
|
||||
path = os.path.join(self.data_path, "esquema_general.json")
|
||||
elif level == "2":
|
||||
path = os.path.join(self.script_groups_path, group, "esquema.json")
|
||||
# Try esquema.json first, then schema.json if not found
|
||||
if not os.path.exists(path):
|
||||
path = os.path.join(self.script_groups_path, group, "schema.json")
|
||||
path = os.path.join(self.script_groups_path, group, "esquema_group.json")
|
||||
elif level == "3":
|
||||
if not group:
|
||||
return {"type": "object", "properties": {}}
|
||||
path = os.path.join(self.script_groups_path, group, "esquema.json")
|
||||
# Try esquema.json first, then schema.json if not found
|
||||
if not os.path.exists(path):
|
||||
path = os.path.join(self.script_groups_path, group, "schema.json")
|
||||
path = os.path.join(self.script_groups_path, group, "esquema_work.json")
|
||||
else:
|
||||
return {"type": "object", "properties": {}}
|
||||
|
||||
|
@ -169,11 +160,11 @@ class ConfigurationManager:
|
|||
try:
|
||||
# Determinar rutas de schema y config
|
||||
if level == "1":
|
||||
schema_path = os.path.join(self.data_path, "esquema.json")
|
||||
schema_path = os.path.join(self.data_path, "esquema_general.json")
|
||||
config_path = os.path.join(self.data_path, "data.json")
|
||||
elif level == "2":
|
||||
schema_path = os.path.join(
|
||||
self.script_groups_path, group, "esquema.json"
|
||||
self.script_groups_path, group, "esquema_group.json"
|
||||
)
|
||||
config_path = os.path.join(self.script_groups_path, group, "data.json")
|
||||
elif level == "3":
|
||||
|
@ -183,7 +174,7 @@ class ConfigurationManager:
|
|||
"message": "Group is required for level 3",
|
||||
}
|
||||
schema_path = os.path.join(
|
||||
self.script_groups_path, group, "esquema.json"
|
||||
self.script_groups_path, group, "esquema_work.json"
|
||||
)
|
||||
config_path = (
|
||||
os.path.join(self.working_directory, "data.json")
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,104 @@
|
|||
.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;
|
||||
width: 50% !important;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
/* Reducción general de padding */
|
||||
.container {
|
||||
padding-left: 1.5rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 1.1rem !important;
|
||||
}
|
||||
|
||||
.px-4 {
|
||||
padding-left: 0.7rem !important;
|
||||
padding-right: 0.7rem !important;
|
||||
}
|
||||
|
||||
.py-8 {
|
||||
padding-top: 1.5rem !important;
|
||||
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 márgenes */
|
||||
.mb-4 {
|
||||
margin-bottom: 0.7rem !important;
|
||||
}
|
||||
|
||||
.mb-6 {
|
||||
margin-bottom: 1.1rem !important;
|
||||
}
|
||||
|
||||
.mb-8 {
|
||||
margin-bottom: 1.5rem !important;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 0.7rem !important;
|
||||
}
|
||||
|
||||
/* Estilos del modal */
|
||||
.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;
|
||||
}
|
|
@ -0,0 +1,847 @@
|
|||
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);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue