diff --git a/backend/script_groups/EmailCrono/.doc/MemoriaDeEvolucion.md b/backend/script_groups/EmailCrono/.doc/MemoriaDeEvolucion.md index b8da862..92034fb 100644 --- a/backend/script_groups/EmailCrono/.doc/MemoriaDeEvolucion.md +++ b/backend/script_groups/EmailCrono/.doc/MemoriaDeEvolucion.md @@ -38,4 +38,21 @@ Salida Markdown: Escribe el índice seguido del contenido formateado en Markdown - Se corrigen lints (E402 y líneas largas) sin alterar lógica. - Impacto: - Los `work_dir.json` existentes deben actualizar la clave a `input_directory`. - - No hay cambios en claves de `level2` (`cronologia_file`, `attachments_dir`). \ No newline at end of file + - No hay cambios en claves de `level2` (`cronologia_file`, `attachments_dir`). + +## 2025-08-08 — Manejo de imágenes (inline y adjuntas) y embebido en Markdown + +- Decisión: + - Capturar imágenes tanto adjuntas (`attachment`) como inline (`inline`/sin `Content-Disposition`). + - Guardar las imágenes en el directorio de adjuntos configurado y además copiar a `adjuntos/cronologia` dentro del `working_directory`. + - Incrustar en el Markdown enlaces de Obsidian con ruta absoluta al archivo copiado en `adjuntos/cronologia` usando la sintaxis de embed `![[...]]` bajo una sección `### Imágenes` por mensaje. +- Cambios: + - `utils/attachment_handler.py`: nueva función `guardar_imagen` que genera nombres a partir de `Content-ID` o hash y evita colisiones por contenido; refactor de hashing de contenido. + - `utils/email_parser.py`: + - Se amplía la firma de `procesar_eml`/`procesar_eml_interno` para recibir `dir_adjuntos_cronologia` y copiar allí las imágenes. + - Se manejan imágenes en partes `attachment` y `inline`, agregando su ruta absoluta copiada a `mensaje.imagenes_cronologia`. + - `models/mensaje_email.py`: `to_markdown()` agrega sección `### Imágenes` con `![[ruta_absoluta]]` previo a `### Adjuntos`. + - `x1.py`: crea `adjuntos/cronologia` y pasa la ruta al parser. +- Impacto: + - El `.md` resultante muestra las imágenes embebidas (Obsidian) desde rutas absolutas bajo `.../adjuntos/cronologia/...`. + - Se preserva el listado de adjuntos como enlaces `[[archivo]]`. \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/models/mensaje_email.py b/backend/script_groups/EmailCrono/models/mensaje_email.py index fb7335b..31520f6 100644 --- a/backend/script_groups/EmailCrono/models/mensaje_email.py +++ b/backend/script_groups/EmailCrono/models/mensaje_email.py @@ -6,7 +6,14 @@ from email.utils import parseaddr, parsedate_to_datetime class MensajeEmail: - def __init__(self, remitente, fecha, contenido, subject=None, adjuntos=None): + 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" @@ -37,17 +44,26 @@ class MensajeEmail: for line in lines: # Skip metadata lines if line.strip().startswith( - ("Da: ", "Inviato: ", "A: ", "From: ", "Sent: ", "To: ") + ( + "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 + # Limpiar espacios múltiples dentro de cada línea, manteniendo + # 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 + # 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 @@ -91,7 +107,10 @@ class MensajeEmail: """ 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}]]" + return ( + f"- {fecha_formato} - {self.remitente} - [[cronologia#" + f"{self.subject}|{subject_link}]]" + ) def _estandarizar_remitente(self, remitente): if "Da:" in remitente: @@ -103,7 +122,8 @@ class MensajeEmail: 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) + patron_nombre = r"([A-Za-z\s]+)\s*<" + nombre_match = re.search(patron_nombre, remitente) if nombre_match: nombre = nombre_match.group(1) else: @@ -117,17 +137,16 @@ class MensajeEmail: if isinstance(fecha, str): try: return parsedate_to_datetime(fecha) - except: + except Exception: 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 + 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 + # Limpiar y normalizar el contenido para el hash (normaliza espacios) contenido_hash = re.sub(r"\s+", " ", self.contenido).strip() # Normalizar el subject @@ -138,13 +157,11 @@ class MensajeEmail: # 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 + # Solo hasta minutos para permitir pequeñas variaciones + self.fecha.strftime("%Y%m%d%H%M"), subject_normalizado, - contenido_hash[ - :500 - ], # Usar solo los primeros 500 caracteres del contenido normalizado + # Usar solo los primeros 500 caracteres del contenido normalizado + contenido_hash[:500], ] # Unir todos los elementos con un separador único @@ -152,12 +169,13 @@ class MensajeEmail: # Mostrar información de debug para el hash (solo si está habilitado) if hasattr(self, "_debug_hash") and self._debug_hash: - print(f" 🔍 Debug Hash:") - print(f" - Remitente: '{self.remitente.strip()}'") - print(f" - Fecha: '{self.fecha.strftime('%Y%m%d%H%M')}'") - print(f" - Subject: '{subject_normalizado}'") - print(f" - Contenido (500 chars): '{contenido_hash[:500]}'") - print(f" - Texto completo hash: '{texto_hash[:100]}...'") + print(" 🔍 Debug Hash:") + print(" - Remitente: '" + self.remitente.strip() + "'") + print(" - Fecha: '" + self.fecha.strftime("%Y%m%d%H%M") + "'") + print(" - Subject: '" + subject_normalizado + "'") + preview = contenido_hash[:500] + print(" - Contenido (500 chars): '" + preview + "'") + print(" - Texto completo hash: '" + texto_hash[:100] + "...'") # Generar el hash hash_resultado = hashlib.md5(texto_hash.encode()).hexdigest() @@ -166,7 +184,8 @@ class MensajeEmail: def debug_hash_info(self): """ - Muestra información detallada sobre cómo se genera el hash de este mensaje + Muestra información detallada de cómo se genera el hash de este + mensaje """ self._debug_hash = True hash_result = self._generar_hash() diff --git a/backend/script_groups/EmailCrono/utils/attachment_handler.py b/backend/script_groups/EmailCrono/utils/attachment_handler.py index 80679cb..946e376 100644 --- a/backend/script_groups/EmailCrono/utils/attachment_handler.py +++ b/backend/script_groups/EmailCrono/utils/attachment_handler.py @@ -3,31 +3,73 @@ import os import hashlib import re + +def _contenido_hash(parte): + contenido = parte.get_payload(decode=True) or b"" + return hashlib.md5(contenido).hexdigest() + + def guardar_adjunto(parte, dir_adjuntos): nombre = parte.get_filename() if not nombre: return None - nombre = re.sub(r'[<>:"/\\|?*]', '_', nombre) + 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_nuevo = _contenido_hash(parte) + 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: + with open(ruta, "wb") as f: f.write(parte.get_payload(decode=True)) - + + return ruta + + +def guardar_imagen(parte, dir_adjuntos): + """ + Guarda una imagen (inline o adjunta). Si no tiene filename, genera uno + basado en Content-ID o hash, preservando la extensión según el subtype. + Devuelve la ruta completa del archivo guardado. + """ + nombre = parte.get_filename() + if not nombre: + # Intentar usar Content-ID + content_id = parte.get("Content-ID", "") or parte.get("Content-Id", "") + content_id = content_id.strip("<>") if content_id else "" + ext = f".{parte.get_content_subtype() or 'bin'}" + base = ( + re.sub(r"[^\w\-]+", "_", content_id) + if content_id + else _contenido_hash(parte) + ) + nombre = f"img_{base}{ext}" + + nombre = re.sub(r'[<>:"/\\|?*]', "_", nombre) + ruta = os.path.join(dir_adjuntos, nombre) + + if os.path.exists(ruta): + hash_nuevo = _contenido_hash(parte) + 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 diff --git a/backend/script_groups/EmailCrono/utils/email_parser.py b/backend/script_groups/EmailCrono/utils/email_parser.py index 1195c85..e3d0af5 100644 --- a/backend/script_groups/EmailCrono/utils/email_parser.py +++ b/backend/script_groups/EmailCrono/utils/email_parser.py @@ -8,8 +8,7 @@ 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 +from utils.attachment_handler import guardar_adjunto, guardar_imagen import os @@ -74,9 +73,45 @@ def _should_skip_line(line): return any(line.strip().startswith(header) for header in headers_to_skip) -def _html_a_markdown(html): +def _find_vault_root(start_path): """ - Convierte contenido HTML a texto markdown, extrayendo el asunto si está presente + Busca hacia arriba un directorio que contenga la carpeta hermana '.obsidian'. + Devuelve la ruta del directorio raíz del vault o None si no se encuentra. + """ + current = os.path.abspath(start_path) + if os.path.isfile(current): + current = os.path.dirname(current) + while True: + obsidian_dir = os.path.join(current, ".obsidian") + if os.path.isdir(obsidian_dir): + return current + parent = os.path.dirname(current) + if parent == current: + return None + current = parent + + +def _ruta_relativa_vault(abs_path): + """ + Convierte una ruta absoluta a una ruta relativa al root del vault Obsidian + si se detecta. Si no se detecta, devuelve la ruta original. + """ + abs_path = os.path.abspath(abs_path) + vault_root = _find_vault_root(abs_path) + if not vault_root: + return abs_path + try: + rel = os.path.relpath(abs_path, vault_root) + # Normalizar separadores a '/' + return rel.replace("\\", "/") + except Exception: + return abs_path + + +def _html_a_markdown(html, cid_to_link=None): + """ + Convierte contenido HTML a texto markdown, extrayendo el asunto si está + presente """ if html is None: return (None, "") @@ -89,6 +124,17 @@ def _html_a_markdown(html): soup = BeautifulSoup(html, "html.parser") + # Reemplazar imágenes inline referenciadas por cid en su lugar + if cid_to_link: + for img in soup.find_all("img"): + src = img.get("src", "") + if src.startswith("cid:"): + cid = src[4:].strip("<>") + embed_path = cid_to_link.get(cid) + if embed_path: + # Obsidian embed (single '!') con ruta relativa al vault + img.replace_with(soup.new_string(f"![[{embed_path}]]")) + # Procesar tablas for table in soup.find_all("table"): try: @@ -126,7 +172,8 @@ def _html_a_markdown(html): rowspan = int(cell.get("rowspan", 1)) colspan = int(cell.get("colspan", 1)) - # Procesar el texto de la celda reemplazando saltos de línea por
+ # Procesar texto de la celda reemplazando saltos de + # línea por
cell_text = cell.get_text().strip() cell_text = cell_text.replace("\n", "
") cell_text = re.sub( @@ -134,17 +181,16 @@ def _html_a_markdown(html): ) # Eliminar
múltiples cell_text = cell_text.strip() - # Rellenar la matriz con el texto y None para las celdas combinadas + # Rellenar la matriz con el texto y None para celdas + # combinadas for r in range(rowspan): current_row = row_idx + r # Expandir matriz si es necesario while len(table_matrix) <= current_row: table_matrix.append([]) - # Expandir fila si es necesario - while ( - len(table_matrix[current_row]) <= col_idx + colspan - 1 - ): - table_matrix[current_row].append(None) + # Expandir fila si es necesario + while len(table_matrix[current_row]) <= col_idx + colspan - 1: + table_matrix[current_row].append(None) for c in range(colspan): if r == 0 and c == 0: @@ -198,9 +244,8 @@ def _html_a_markdown(html): # Reemplazar la tabla HTML con la versión Markdown if markdown_table: - table.replace_with( - soup.new_string("\n" + "\n".join(markdown_table) + "\n") - ) + replacement = "\n" + "\n".join(markdown_table) + "\n" + table.replace_with(soup.new_string(replacement)) except Exception as e: print(f"Error procesando tabla: {str(e)}") @@ -232,7 +277,7 @@ def _html_a_markdown(html): return (None, html if html else "") -def _procesar_email_adjunto(parte, dir_adjuntos): +def _procesar_email_adjunto(parte, dir_adjuntos, dir_adjuntos_cronologia=None): """ Procesa un email que viene como adjunto dentro de otro email. """ @@ -246,17 +291,29 @@ def _procesar_email_adjunto(parte, dir_adjuntos): payload = subparte.get_payload() if isinstance(payload, list): for msg in payload: - mensajes.extend(procesar_eml_interno(msg, dir_adjuntos)) + mensajes.extend( + procesar_eml_interno( + msg, dir_adjuntos, dir_adjuntos_cronologia + ) + ) elif isinstance(payload, email.message.Message): - mensajes.extend(procesar_eml_interno(payload, dir_adjuntos)) + mensajes.extend( + procesar_eml_interno( + payload, dir_adjuntos, dir_adjuntos_cronologia + ) + ) 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)) + mensajes.extend( + procesar_eml_interno(msg, dir_adjuntos, dir_adjuntos_cronologia) + ) elif isinstance(payload, email.message.Message): - mensajes.extend(procesar_eml_interno(payload, dir_adjuntos)) + mensajes.extend( + procesar_eml_interno(payload, dir_adjuntos, dir_adjuntos_cronologia) + ) return mensajes except Exception as e: @@ -264,7 +321,7 @@ def _procesar_email_adjunto(parte, dir_adjuntos): return [] -def procesar_eml(ruta_archivo, dir_adjuntos): +def procesar_eml(ruta_archivo, dir_adjuntos, dir_adjuntos_cronologia=None): """ Punto de entrada principal para procesar archivos .eml """ @@ -273,7 +330,7 @@ def procesar_eml(ruta_archivo, dir_adjuntos): with open(ruta_archivo, "rb") as eml: mensaje = BytesParser(policy=policy.default).parse(eml) - mensajes = procesar_eml_interno(mensaje, dir_adjuntos) + mensajes = procesar_eml_interno(mensaje, dir_adjuntos, dir_adjuntos_cronologia) print(f" 📧 Procesamiento completado: {len(mensajes)} mensajes extraídos") return mensajes except Exception as e: @@ -281,7 +338,7 @@ def procesar_eml(ruta_archivo, dir_adjuntos): return [] -def procesar_eml_interno(mensaje, dir_adjuntos): +def procesar_eml_interno(mensaje, dir_adjuntos, dir_adjuntos_cronologia=None): """ Procesa un mensaje de email, ya sea desde archivo o adjunto """ @@ -296,13 +353,15 @@ def procesar_eml_interno(mensaje, dir_adjuntos): subject = mensaje.get("subject", "") if subject: # Try to decode if it's encoded - subject = str(email.header.make_header(email.header.decode_header(subject))) + decoded = email.header.decode_header(subject) + subject = str(email.header.make_header(decoded)) contenido = "" adjuntos = [] + imagenes = [] tiene_html = False - # First pass: check for HTML content + # Primer pase: detectar si hay HTML if mensaje.is_multipart(): for parte in mensaje.walk(): if parte.get_content_type() == "text/html": @@ -311,10 +370,29 @@ def procesar_eml_interno(mensaje, dir_adjuntos): else: tiene_html = mensaje.get_content_type() == "text/html" - # Second pass: process content and attachments + # Segundo pase: procesar contenido y adjuntos if mensaje.is_multipart(): - # Asegurarnos de capturar SOLO una vez el cuerpo principal y no sobrescribirlo + # Capturar SOLO una vez el cuerpo principal y no sobrescribirlo contenido_set = False # flag para no re-asignar contenido principal + # Construir mapa cid->ruta de embed (relativa al vault) para inline + cid_to_link = {} + if dir_adjuntos_cronologia: + for parte in mensaje.walk(): + ctype = parte.get_content_type() + dispo = parte.get_content_disposition() + if ctype.startswith("image/") and dispo in (None, "inline"): + cid_header = parte.get("Content-ID", "") or parte.get( + "Content-Id", "" + ) + if cid_header: + cid_clean = cid_header.strip("<>") + # Guardar SOLO en adjuntos/cronologia + ruta_img = guardar_imagen(parte, dir_adjuntos_cronologia) + if ruta_img: + # Convertir a ruta relativa al vault + embed_path = _ruta_relativa_vault(ruta_img) + cid_to_link[cid_clean] = embed_path + for parte in mensaje.walk(): content_type = parte.get_content_type() @@ -330,7 +408,9 @@ def procesar_eml_interno(mensaje, dir_adjuntos): if content_type == "text/html": html_content = _get_payload_safely(parte) if html_content: - part_subject, text = _html_a_markdown(html_content) + part_subject, text = _html_a_markdown( + html_content, cid_to_link + ) if not subject and part_subject: subject = part_subject if text: @@ -346,7 +426,9 @@ def procesar_eml_interno(mensaje, dir_adjuntos): # 2. EMAILS RFC822 ADJUNTOS # ----------------------------- elif content_type == "message/rfc822": - mensajes_adjuntos = _procesar_email_adjunto(parte, dir_adjuntos) + mensajes_adjuntos = _procesar_email_adjunto( + parte, dir_adjuntos, dir_adjuntos_cronologia + ) mensajes.extend(mensajes_adjuntos) # ----------------------------- @@ -356,13 +438,27 @@ def procesar_eml_interno(mensaje, dir_adjuntos): nombre = parte.get_filename() if nombre and nombre.lower().endswith(".eml"): mensajes_adjuntos = _procesar_email_adjunto( - parte, dir_adjuntos + parte, dir_adjuntos, dir_adjuntos_cronologia ) mensajes.extend(mensajes_adjuntos) else: - ruta_adjunto = guardar_adjunto(parte, dir_adjuntos) - if ruta_adjunto: - adjuntos.append(Path(ruta_adjunto).name) + # Imagen adjunta (no inline): solo guardar en adjuntos + if content_type.startswith("image/"): + ruta_img = guardar_imagen(parte, dir_adjuntos) + if ruta_img: + adjuntos.append(Path(ruta_img).name) + else: + ruta_adjunto = guardar_adjunto(parte, dir_adjuntos) + if ruta_adjunto: + adjuntos.append(Path(ruta_adjunto).name) + + # 4. IMÁGENES INLINE: ya manejadas para embebido; no listar + elif content_type.startswith("image/") and ( + parte.get_content_disposition() in (None, "inline") + ): + # Nada que hacer aquí; ya se guardó en cronologia y + # se reemplazó en el HTML + pass except Exception as e: print(f"Error procesando parte del mensaje: {str(e)}") @@ -371,7 +467,8 @@ def procesar_eml_interno(mensaje, dir_adjuntos): if mensaje.get_content_type() == "text/html": html_content = _get_payload_safely(mensaje) if html_content: - part_subject, contenido = _html_a_markdown(html_content) + # Para mensajes no multipart, no hay inline cid a resolver + part_subject, contenido = _html_a_markdown(html_content, {}) if not subject and part_subject: subject = part_subject else: @@ -386,7 +483,7 @@ def procesar_eml_interno(mensaje, dir_adjuntos): subject=subject, adjuntos=adjuntos, ) - print(f" ✉️ Mensaje extraído:") + print(" ✉️ Mensaje extraído:") print(f" - Subject: {subject}") print(f" - Remitente: {remitente}") print(f" - Fecha: {fecha}") @@ -395,7 +492,7 @@ def procesar_eml_interno(mensaje, dir_adjuntos): print(f" - Hash generado: {mensaje_nuevo.hash}") mensajes.append(mensaje_nuevo) else: - print(f" ⚠️ Mensaje vacío o sin contenido útil - no se agregará") + print(" ⚠️ Mensaje vacío o sin contenido útil - no se agregará") except Exception as e: print(f"Error procesando mensaje: {str(e)}") @@ -407,7 +504,7 @@ def _parsear_fecha(fecha_str): try: fecha = parsedate_to_datetime(fecha_str) return fecha.replace(tzinfo=None) # Remove timezone info - except: + except Exception: try: fecha_match = re.search( r"venerd=EC (\d{1,2}) (\w+) (\d{4}) (\d{1,2}):(\d{2})", fecha_str @@ -430,6 +527,6 @@ def _parsear_fecha(fecha_str): } mes_num = meses_it.get(mes.lower(), 1) return datetime(int(año), mes_num, int(dia), int(hora), int(minuto)) - except: + except Exception: pass return datetime.now() diff --git a/backend/script_groups/EmailCrono/x1.py b/backend/script_groups/EmailCrono/x1.py index f1c7f63..d76452c 100644 --- a/backend/script_groups/EmailCrono/x1.py +++ b/backend/script_groups/EmailCrono/x1.py @@ -62,6 +62,7 @@ def main(): # Construir rutas de salida en working_directory output_file = os.path.join(working_directory, cronologia_file) attachments_path = os.path.join(working_directory, attachments_dir) + attachments_crono_path = os.path.join(attachments_path, "cronologia") # Debug prints print(f"Working/Output directory: {working_directory}") @@ -80,6 +81,7 @@ def main(): # Asegurar directorios de salida os.makedirs(working_directory, exist_ok=True) os.makedirs(attachments_path, exist_ok=True) + os.makedirs(attachments_crono_path, exist_ok=True) # Check if input directory exists and has files input_path = Path(input_dir) @@ -110,7 +112,9 @@ def main(): print(f"\n{'='*60}") print(f"Processing file: {archivo}") sys.stdout.flush() - nuevos_mensajes = procesar_eml(archivo, attachments_path) + nuevos_mensajes = procesar_eml( + archivo, attachments_path, attachments_crono_path + ) print(f"Extracted {len(nuevos_mensajes)} messages from {archivo.name}") sys.stdout.flush() total_procesados += len(nuevos_mensajes) diff --git a/backend/script_groups/OllamaTools/templates/index.html b/backend/script_groups/OllamaTools/templates/index.html index 610f9f0..47db08b 100644 --- a/backend/script_groups/OllamaTools/templates/index.html +++ b/backend/script_groups/OllamaTools/templates/index.html @@ -135,6 +135,21 @@ position: relative; } + select { + padding: 12px 16px; + border: 2px solid #e2e8f0; + border-radius: 8px; + font-size: 1em; + background: white; + transition: border-color 0.3s ease; + } + + select:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); + } + input[type="text"] { padding: 12px 16px; border: 2px solid #e2e8f0; @@ -461,6 +476,18 @@ +
+ + +
@@ -497,6 +524,7 @@ // Estado global de la aplicación let currentModels = []; let isLoading = false; + let currentSort = 'modified_desc'; // Inicializar aplicación document.addEventListener('DOMContentLoaded', function () { @@ -509,6 +537,16 @@ downloadModel(); } }); + + // Configurar selector de orden + const sortSelect = document.getElementById('sort-select'); + if (sortSelect) { + sortSelect.value = currentSort; + sortSelect.addEventListener('change', function () { + currentSort = this.value; + applySortAndRender(); + }); + } }); // Verificar estado de Ollama @@ -557,7 +595,7 @@ if (data.status === 'success') { currentModels = data.models || []; updateStats(data); - renderModels(currentModels); + applySortAndRender(); } else { showError(`Error al cargar modelos: ${data.message}`); container.innerHTML = ` @@ -578,6 +616,34 @@ } } + // Aplicar orden y renderizar + function applySortAndRender() { + const sorted = [...currentModels]; + const getSize = (m) => typeof m.size === 'number' ? m.size : (parseInt(m.size, 10) || 0); + const getModifiedTs = (m) => m.modified_at ? (Date.parse(m.modified_at) || 0) : 0; + + switch (currentSort) { + case 'size_desc': + sorted.sort((a, b) => getSize(b) - getSize(a)); + break; + case 'size_asc': + sorted.sort((a, b) => getSize(a) - getSize(b)); + break; + case 'modified_asc': + sorted.sort((a, b) => getModifiedTs(a) - getModifiedTs(b)); + break; + case 'name_asc': + sorted.sort((a, b) => (a.name || '').localeCompare(b.name || '')); + break; + case 'modified_desc': + default: + sorted.sort((a, b) => getModifiedTs(b) - getModifiedTs(a)); + break; + } + + renderModels(sorted); + } + // Actualizar estadísticas function updateStats(data) { const statsElement = document.getElementById('stats'); diff --git a/data/launcher_history.json b/data/launcher_history.json index 7f03814..00d322c 100644 --- a/data/launcher_history.json +++ b/data/launcher_history.json @@ -1,5 +1,31 @@ { "history": [ + { + "id": "286de385", + "group_id": "2", + "script_name": "main.py", + "executed_date": "2025-08-08T13:31:57.810744Z", + "arguments": [], + "working_directory": "D:/Proyectos/Scripts/RS485/MaselliSimulatorApp", + "python_env": "tia_scripting", + "executable_type": "pythonw.exe", + "status": "running", + "pid": 63664, + "execution_time": null + }, + { + "id": "19b3a39f", + "group_id": "2", + "script_name": "main.py", + "executed_date": "2025-08-08T12:08:20.490182Z", + "arguments": [], + "working_directory": "D:/Proyectos/Scripts/RS485/MaselliSimulatorApp", + "python_env": "tia_scripting", + "executable_type": "pythonw.exe", + "status": "running", + "pid": 66300, + "execution_time": null + }, { "id": "23248897", "group_id": "2", diff --git a/data/log.txt b/data/log.txt index ac3059a..6fb9e58 100644 --- a/data/log.txt +++ b/data/log.txt @@ -1,107 +1,107 @@ -[11:43:49] Iniciando ejecución de x1.py en C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\17 - E5.006880 - Modifica O&U - RSC098... -[11:43:49] ✅ Configuración cargada exitosamente -[11:43:49] Working/Output directory: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\17 - E5.006880 - Modifica O&U - RSC098 -[11:43:49] Input directory: C:\Trabajo\SIDEL\17 - E5.006880 - Modifica O&U - RSC098\Reporte\Emails -[11:43:49] Output file: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\17 - E5.006880 - Modifica O&U - RSC098\cronologia.md -[11:43:49] Attachments directory: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\17 - E5.006880 - Modifica O&U - RSC098\adjuntos -[11:43:49] Beautify rules file: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\EmailCrono\config\beautify_rules.json -[11:43:49] Found 1 .eml files -[11:43:49] Creando cronología nueva (archivo se sobrescribirá) -[11:43:49] ============================================================ -[11:43:49] Processing file: C:\Trabajo\SIDEL\17 - E5.006880 - Modifica O&U - RSC098\Reporte\Emails\R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml -[11:43:49] 📧 Abriendo archivo: C:\Trabajo\SIDEL\17 - E5.006880 - Modifica O&U - RSC098\Reporte\Emails\R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml -[11:43:49] ✉️ Mensaje extraído: -[11:43:49] - Subject: R: {EXT} R: E5.006880 - RSC098 - Nigerian Breweries: URGENT -[11:43:49] - Remitente: "Passera, Alessandro" -[11:43:49] - Fecha: 2025-08-08 07:49:28 -[11:43:49] - Adjuntos: 0 archivos -[11:43:49] - Contenido: 4735 caracteres -[11:43:49] - Hash generado: 48f94bf24945f73bc08c1c0cf8c1e8bb -[11:43:49] ✉️ Mensaje extraído: -[11:43:49] - Subject: RE: {EXT} R: E5.006880 - RSC098 - Nigerian Breweries: URGENT -[11:43:49] - Remitente: "Bii, Vickodyne" -[11:43:49] - Fecha: 2025-08-08 05:46:30 -[11:43:49] - Adjuntos: 0 archivos -[11:43:49] - Contenido: 4259 caracteres -[11:43:49] - Hash generado: 82709d4677b90d79bb02e13cfe86924e -[11:43:49] ✉️ Mensaje extraído: -[11:43:49] - Subject: R: E5.006880 - RSC098 - Nigerian Breweries: URGENT -[11:43:49] - Remitente: "walter.orsi@teknors.com" -[11:43:49] - Fecha: 2025-08-07 15:55:58 -[11:43:49] - Adjuntos: 0 archivos -[11:43:49] - Contenido: 3235 caracteres -[11:43:49] - Hash generado: cceec9818de1a4491214af4b6d96e143 -[11:43:49] ✉️ Mensaje extraído: -[11:43:49] - Subject: R: E5.006880 - RSC098 - Nigerian Breweries: URGENT -[11:43:49] - Remitente: "Passera, Alessandro" -[11:43:49] - Fecha: 2025-08-07 13:07:11 -[11:43:49] - Adjuntos: 0 archivos -[11:43:49] - Contenido: 2398 caracteres -[11:43:49] - Hash generado: dc05b2959920f679cd60e8a29685badc -[11:43:49] ✉️ Mensaje extraído: -[11:43:49] - Subject: R: E5.006880 - RSC098 - Nigerian Breweries: URGENT -[11:43:49] - Remitente: "Passera, Alessandro" -[11:43:49] - Fecha: 2025-08-07 12:59:15 -[11:43:49] - Adjuntos: 0 archivos -[11:43:49] - Contenido: 1613 caracteres -[11:43:49] - Hash generado: a848be3351ae2cc44bafb0f322a78690 -[11:43:49] ✉️ Mensaje extraído: -[11:43:49] - Subject: RE: {EXT} R: E5.006880 - RSC098 - Nigerian Breweries: URGENT -[11:43:49] - Remitente: Miguel Angel Vera -[11:43:49] - Fecha: 2025-08-08 09:41:58 -[11:43:49] - Adjuntos: 0 archivos -[11:43:49] - Contenido: 4735 caracteres -[11:43:49] - Hash generado: 430cc918020c3c8db795995baa26cb78 -[11:43:49] 📧 Procesamiento completado: 6 mensajes extraídos -[11:43:49] Extracted 6 messages from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml -[11:43:49] --- Msg 1/6 from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml --- -[11:43:49] Remitente: Passera, Alessandro -[11:43:49] Fecha: 2025-08-08 07:49:28 -[11:43:49] Subject: R: {EXT} R: E5.006880 - RSC098 - Nigerian Breweries: URGENT -[11:43:49] Hash: 48f94bf24945f73bc08c1c0cf8c1e8bb -[11:43:49] Adjuntos: [] -[11:43:49] ✓ NUEVO mensaje - Agregando a la cronología -[11:43:49] --- Msg 2/6 from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml --- -[11:43:49] Remitente: Bii, Vickodyne -[11:43:49] Fecha: 2025-08-08 05:46:30 -[11:43:49] Subject: RE: {EXT} R: E5.006880 - RSC098 - Nigerian Breweries: URGENT -[11:43:49] Hash: 82709d4677b90d79bb02e13cfe86924e -[11:43:49] Adjuntos: [] -[11:43:49] ✓ NUEVO mensaje - Agregando a la cronología -[11:43:49] --- Msg 3/6 from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml --- -[11:43:49] Remitente: walter.orsi@teknors.com -[11:43:49] Fecha: 2025-08-07 15:55:58 -[11:43:49] Subject: R: E5.006880 - RSC098 - Nigerian Breweries: URGENT -[11:43:49] Hash: cceec9818de1a4491214af4b6d96e143 -[11:43:49] Adjuntos: [] -[11:43:49] ✓ NUEVO mensaje - Agregando a la cronología -[11:43:49] --- Msg 4/6 from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml --- -[11:43:49] Remitente: Passera, Alessandro -[11:43:49] Fecha: 2025-08-07 13:07:11 -[11:43:49] Subject: R: E5.006880 - RSC098 - Nigerian Breweries: URGENT -[11:43:49] Hash: dc05b2959920f679cd60e8a29685badc -[11:43:49] Adjuntos: [] -[11:43:49] ✓ NUEVO mensaje - Agregando a la cronología -[11:43:49] --- Msg 5/6 from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml --- -[11:43:49] Remitente: Passera, Alessandro -[11:43:49] Fecha: 2025-08-07 12:59:15 -[11:43:49] Subject: R: E5.006880 - RSC098 - Nigerian Breweries: URGENT -[11:43:49] Hash: a848be3351ae2cc44bafb0f322a78690 -[11:43:49] Adjuntos: [] -[11:43:49] ✓ NUEVO mensaje - Agregando a la cronología -[11:43:49] --- Msg 6/6 from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml --- -[11:43:49] Remitente: Miguel Angel Vera -[11:43:49] Fecha: 2025-08-08 09:41:58 -[11:43:49] Subject: RE: {EXT} R: E5.006880 - RSC098 - Nigerian Breweries: URGENT -[11:43:49] Hash: 430cc918020c3c8db795995baa26cb78 -[11:43:49] Adjuntos: [] -[11:43:49] ✓ NUEVO mensaje - Agregando a la cronología -[11:43:49] Estadísticas de procesamiento: -[11:43:49] - Total mensajes encontrados: 6 -[11:43:49] - Mensajes únicos añadidos: 6 -[11:43:49] - Mensajes duplicados ignorados: 0 -[11:43:49] Writing 6 messages to C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\17 - E5.006880 - Modifica O&U - RSC098\cronologia.md -[11:43:49] ✅ Cronología guardada exitosamente en: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\17 - E5.006880 - Modifica O&U - RSC098\cronologia.md -[11:43:49] 📊 Total de mensajes en la cronología: 6 -[11:43:49] Ejecución de x1.py finalizada (success). Duración: 0:00:00.353973. -[11:43:49] Log completo guardado en: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\EmailCrono\.log\log_x1.txt +[14:55:47] Iniciando ejecución de x1.py en C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\17 - E5.006880 - Modifica O&U - RSC098... +[14:55:47] ✅ Configuración cargada exitosamente +[14:55:47] Working/Output directory: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\17 - E5.006880 - Modifica O&U - RSC098 +[14:55:47] Input directory: C:\Trabajo\SIDEL\17 - E5.006880 - Modifica O&U - RSC098\Reporte\Emails +[14:55:47] Output file: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\17 - E5.006880 - Modifica O&U - RSC098\cronologia.md +[14:55:47] Attachments directory: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\17 - E5.006880 - Modifica O&U - RSC098\adjuntos +[14:55:47] Beautify rules file: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\EmailCrono\config\beautify_rules.json +[14:55:47] Found 1 .eml files +[14:55:47] Creando cronología nueva (archivo se sobrescribirá) +[14:55:47] ============================================================ +[14:55:47] Processing file: C:\Trabajo\SIDEL\17 - E5.006880 - Modifica O&U - RSC098\Reporte\Emails\R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml +[14:55:47] 📧 Abriendo archivo: C:\Trabajo\SIDEL\17 - E5.006880 - Modifica O&U - RSC098\Reporte\Emails\R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml +[14:55:48] ✉️ Mensaje extraído: +[14:55:48] - Subject: R: {EXT} R: E5.006880 - RSC098 - Nigerian Breweries: URGENT +[14:55:48] - Remitente: "Passera, Alessandro" +[14:55:48] - Fecha: 2025-08-08 07:49:28 +[14:55:48] - Adjuntos: 0 archivos +[14:55:48] - Contenido: 5160 caracteres +[14:55:48] - Hash generado: 48f94bf24945f73bc08c1c0cf8c1e8bb +[14:55:48] ✉️ Mensaje extraído: +[14:55:48] - Subject: RE: {EXT} R: E5.006880 - RSC098 - Nigerian Breweries: URGENT +[14:55:48] - Remitente: "Bii, Vickodyne" +[14:55:48] - Fecha: 2025-08-08 05:46:30 +[14:55:48] - Adjuntos: 0 archivos +[14:55:48] - Contenido: 4686 caracteres +[14:55:48] - Hash generado: 352a3d37ac274b11822ca5527cd8865b +[14:55:48] ✉️ Mensaje extraído: +[14:55:48] - Subject: R: E5.006880 - RSC098 - Nigerian Breweries: URGENT +[14:55:48] - Remitente: "walter.orsi@teknors.com" +[14:55:48] - Fecha: 2025-08-07 15:55:58 +[14:55:48] - Adjuntos: 0 archivos +[14:55:48] - Contenido: 3583 caracteres +[14:55:48] - Hash generado: b2558be8631ba7d14210a4a3379dfdad +[14:55:48] ✉️ Mensaje extraído: +[14:55:48] - Subject: R: E5.006880 - RSC098 - Nigerian Breweries: URGENT +[14:55:48] - Remitente: "Passera, Alessandro" +[14:55:48] - Fecha: 2025-08-07 13:07:11 +[14:55:48] - Adjuntos: 0 archivos +[14:55:48] - Contenido: 2485 caracteres +[14:55:48] - Hash generado: dc05b2959920f679cd60e8a29685badc +[14:55:48] ✉️ Mensaje extraído: +[14:55:48] - Subject: R: E5.006880 - RSC098 - Nigerian Breweries: URGENT +[14:55:48] - Remitente: "Passera, Alessandro" +[14:55:48] - Fecha: 2025-08-07 12:59:15 +[14:55:48] - Adjuntos: 0 archivos +[14:55:48] - Contenido: 1700 caracteres +[14:55:48] - Hash generado: a848be3351ae2cc44bafb0f322a78690 +[14:55:48] ✉️ Mensaje extraído: +[14:55:48] - Subject: RE: {EXT} R: E5.006880 - RSC098 - Nigerian Breweries: URGENT +[14:55:48] - Remitente: Miguel Angel Vera +[14:55:48] - Fecha: 2025-08-08 09:41:58 +[14:55:48] - Adjuntos: 0 archivos +[14:55:48] - Contenido: 5160 caracteres +[14:55:48] - Hash generado: 430cc918020c3c8db795995baa26cb78 +[14:55:48] 📧 Procesamiento completado: 6 mensajes extraídos +[14:55:48] Extracted 6 messages from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml +[14:55:48] --- Msg 1/6 from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml --- +[14:55:48] Remitente: Passera, Alessandro +[14:55:48] Fecha: 2025-08-08 07:49:28 +[14:55:48] Subject: R: {EXT} R: E5.006880 - RSC098 - Nigerian Breweries: URGENT +[14:55:48] Hash: 48f94bf24945f73bc08c1c0cf8c1e8bb +[14:55:48] Adjuntos: [] +[14:55:48] ✓ NUEVO mensaje - Agregando a la cronología +[14:55:48] --- Msg 2/6 from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml --- +[14:55:48] Remitente: Bii, Vickodyne +[14:55:48] Fecha: 2025-08-08 05:46:30 +[14:55:48] Subject: RE: {EXT} R: E5.006880 - RSC098 - Nigerian Breweries: URGENT +[14:55:48] Hash: 352a3d37ac274b11822ca5527cd8865b +[14:55:48] Adjuntos: [] +[14:55:48] ✓ NUEVO mensaje - Agregando a la cronología +[14:55:48] --- Msg 3/6 from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml --- +[14:55:48] Remitente: walter.orsi@teknors.com +[14:55:48] Fecha: 2025-08-07 15:55:58 +[14:55:48] Subject: R: E5.006880 - RSC098 - Nigerian Breweries: URGENT +[14:55:48] Hash: b2558be8631ba7d14210a4a3379dfdad +[14:55:48] Adjuntos: [] +[14:55:48] ✓ NUEVO mensaje - Agregando a la cronología +[14:55:48] --- Msg 4/6 from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml --- +[14:55:48] Remitente: Passera, Alessandro +[14:55:48] Fecha: 2025-08-07 13:07:11 +[14:55:48] Subject: R: E5.006880 - RSC098 - Nigerian Breweries: URGENT +[14:55:48] Hash: dc05b2959920f679cd60e8a29685badc +[14:55:48] Adjuntos: [] +[14:55:48] ✓ NUEVO mensaje - Agregando a la cronología +[14:55:48] --- Msg 5/6 from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml --- +[14:55:48] Remitente: Passera, Alessandro +[14:55:48] Fecha: 2025-08-07 12:59:15 +[14:55:48] Subject: R: E5.006880 - RSC098 - Nigerian Breweries: URGENT +[14:55:48] Hash: a848be3351ae2cc44bafb0f322a78690 +[14:55:48] Adjuntos: [] +[14:55:48] ✓ NUEVO mensaje - Agregando a la cronología +[14:55:48] --- Msg 6/6 from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml --- +[14:55:48] Remitente: Miguel Angel Vera +[14:55:48] Fecha: 2025-08-08 09:41:58 +[14:55:48] Subject: RE: {EXT} R: E5.006880 - RSC098 - Nigerian Breweries: URGENT +[14:55:48] Hash: 430cc918020c3c8db795995baa26cb78 +[14:55:48] Adjuntos: [] +[14:55:48] ✓ NUEVO mensaje - Agregando a la cronología +[14:55:48] Estadísticas de procesamiento: +[14:55:48] - Total mensajes encontrados: 6 +[14:55:48] - Mensajes únicos añadidos: 6 +[14:55:48] - Mensajes duplicados ignorados: 0 +[14:55:48] Writing 6 messages to C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\17 - E5.006880 - Modifica O&U - RSC098\cronologia.md +[14:55:48] ✅ Cronología guardada exitosamente en: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\17 - E5.006880 - Modifica O&U - RSC098\cronologia.md +[14:55:48] 📊 Total de mensajes en la cronología: 6 +[14:55:48] Ejecución de x1.py finalizada (success). Duración: 0:00:00.497219. +[14:55:48] Log completo guardado en: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\EmailCrono\.log\log_x1.txt