Se añadió la creación del directorio `adjuntos/cronologia` en `x1.py` para almacenar imágenes de correos electrónicos. Se actualizó la función `procesar_eml` para manejar imágenes inline y adjuntas, y se refactorizó el código en `email_parser.py` para incluir la lógica de incrustación de imágenes en Markdown. Además, se mejoró la documentación en `MemoriaDeEvolucion.md` para reflejar estos cambios y se optimizó el manejo de errores en varias funciones.

This commit is contained in:
Miguel 2025-08-08 15:07:32 +02:00
parent 59cb4f4063
commit fc85347a43
8 changed files with 451 additions and 180 deletions

View File

@ -39,3 +39,20 @@ Salida Markdown: Escribe el índice seguido del contenido formateado en Markdown
- Impacto:
- Los `work_dir.json` existentes deben actualizar la clave a `input_directory`.
- 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]]`.

View File

@ -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()

View File

@ -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

View File

@ -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 <br>
# Procesar texto de la celda reemplazando saltos de
# línea por <br>
cell_text = cell.get_text().strip()
cell_text = cell_text.replace("\n", "<br>")
cell_text = re.sub(
@ -134,17 +181,16 @@ def _html_a_markdown(html):
) # Eliminar <br> 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()

View File

@ -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)

View File

@ -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 @@
<button class="btn btn-secondary" onclick="refreshModels()">
🔄 Actualizar
</button>
<div class="input-group">
<label for="sort-select"
style="margin-left:10px; margin-right:6px; color:#4a5568; font-weight:600;">Ordenar
por:</label>
<select id="sort-select">
<option value="modified_desc">Última modificación (recientes primero)</option>
<option value="modified_asc">Última modificación (antiguos primero)</option>
<option value="size_desc">Tamaño (grandes primero)</option>
<option value="size_asc">Tamaño (pequeños primero)</option>
<option value="name_asc">Nombre (A-Z)</option>
</select>
</div>
</div>
</div>
@ -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');

View File

@ -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",

View File

@ -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" <Alessandro.Passera@sidel.com>
[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" <vickodyne.bii@sidel.com>
[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" <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" <Alessandro.Passera@sidel.com>
[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" <Alessandro.Passera@sidel.com>
[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 <miguelverateknors@gmail.com>
[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" <Alessandro.Passera@sidel.com>
[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" <vickodyne.bii@sidel.com>
[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" <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" <Alessandro.Passera@sidel.com>
[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" <Alessandro.Passera@sidel.com>
[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 <miguelverateknors@gmail.com>
[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