almeira-web/docs/developer_context.md

9.1 KiB

Contexto de Desarrollo y Arquitectura - Almeira Web

Este archivo sirve como memoria técnica e histórica para los desarrolladores y agentes de IA (como Antigravity) que continúen trabajando en este proyecto.


🛠️ Stack Tecnológico

  • Core: Next.js 16 (App Router, con Server Actions habilitados).
  • Base de Datos y Backend: Appwrite (Autenticación, Base de datos y Almacenamiento).
  • Editor de Contenido: Tiptap Editor (Rich Text con soporte para imágenes, videos y embeds).
  • Estilos: TailwindCSS.
  • Empaquetado: Node standalone.

📂 Configuración del Entorno (.env.local)

Para levantar el proyecto en desarrollo local (npm run dev), se requiere un archivo .env.local en la raíz con el siguiente formato:

NEXT_PUBLIC_APPWRITE_ENDPOINT=https://appwrite.almeira.com.co/v1
NEXT_PUBLIC_APPWRITE_PROJECT_ID=69b84df5003bd8957c5c
APPWRITE_API_KEY=standard_cce3cd... (Ver Gitea o Consola Appwrite)
APPWRITE_DATABASE_ID=almeira
NEXT_PUBLIC_APPWRITE_MEDIA_BUCKET_ID=media_bucket

# Evolution API (WhatsApp)
EVOLUTION_API_URL=https://evolution.almeira.com.co
EVOLUTION_API_KEY=mgXxu2eA...
EVOLUTION_INSTANCE_NAME=AutomataAlmeira

# Server Config
SERVER_ACTIONS_BODY_SIZE_LIMIT=100mb

🎬 Solución del Reproductor de Video (Hito Clave)

Durante el desarrollo, se resolvió un problema crítico donde los videos subidos a las publicaciones no se reproducían en producción:

1. El Origen del Problema

Appwrite utiliza el endpoint /preview para imágenes y miniaturas. Sin embargo, para archivos de video, el navegador requiere soporte de reproducción por rangos (Range Requests) y streaming. El endpoint /preview limita la reproducción y bloquea el video en Safari y Chrome. La solución es usar el endpoint /view.

2. Implementación de la Corrección

  • En el Editor (RichTextEditor.tsx / MediaGridSelector.tsx): Se configuró la biblioteca de medios y el nodo de video de Tiptap para reemplazar automáticamente /preview? por /view? en las URLs de origen de los videos.
  • En el Servidor (entities.ts): Se actualizó el validador safeHtmlSchema que limpia el código HTML con DOMPurify para permitir las etiquetas <video> y <source>, junto a los atributos controls, src, muted, loop, autoplay, preload y poster. Además, normaliza cualquier URL de video errónea antes de guardarla en la base de datos.
  • En la Publicación ([slug]/page.tsx): Se implementó una mejora dinámica que extrae el primer video del contenido y lo coloca como Hero Media en la cabecera (usando la imagen destacada como poster de portada), eliminando el video redundante del cuerpo del texto. También normaliza URLs antiguas al vuelo.

🔄 Scripts de Despliegue y Compilación

  • package-hostinger.sh: Script Bash que compila el proyecto en modo standalone, copia los assets estáticos de .next/static y public/, inyecta la configuración para PM2 (ecosystem.config.js), configura el package.json para que Hostinger corra directamente server.js y comprime todo en un archivo zip listo para subir: almeira-web-deploy.zip.

Comando para empaquetar:

PATH="/home/carlos/.nvm/versions/node/v22.14.0/bin:$PATH" bash package-hostinger.sh

💡 Instrucciones para Agentes de IA (Continuar Trabajo)

Si eres un asistente de codificación de IA (como Antigravity), ten en cuenta lo siguiente al iniciar:

  1. Este proyecto utiliza Next.js standalone. Cualquier cambio en las rutas o componentes debe comprobarse compilando el proyecto localmente.
  2. La sanitización de HTML es muy estricta. Si necesitas añadir nuevos tags interactivos a las publicaciones, debes agregarlos al whitelist de safeHtmlSchema en src/app/actions/entities.ts y al sanitizador en src/app/[slug]/page.tsx.

🏛️ Historial de Hitos Técnicos y Errores Resueltos (Logs de Sesiones)

A lo largo del desarrollo de la plataforma, se han resuelto varios problemas críticos de arquitectura y renderizado. A continuación, se detallan para evitar regresiones:

1. Error de Serialización de Componentes Servidor a Cliente ("Only plain objects...")

  • Problema: Al intentar pasar colecciones de documentos directamente desde las Server Actions o Server Components de Next.js a componentes cliente (como NewsCard o GlobalSidebar), Next.js arrojaba el error: "Only plain objects can be passed to Client Components". Esto ocurría porque los SDKs de Appwrite y ciertas funciones devuelven instancias de clase complejas u objetos con métodos prototípicos no serializables.
  • Solución: Se implementó una política estricta de desestructuración y serialización profunda en los flujos de datos. En las Server Actions (por ejemplo, en src/app/actions/entities.ts y al consultar datos en Home), se aplica un parseo profundo JSON.parse(JSON.stringify(data)) o mapeo explícito de propiedades antes de retornar los datos al cliente.

2. Resolución de IDs de Media y Logotipo Corporativo Roto

  • Problema: El logotipo corporativo y varios recursos multimedia en la interfaz fallaban en renderizarse de forma intermitente debido a inconsistencias al resolver los IDs de documentos de Appwrite frente a los IDs reales de los archivos en Storage.
  • Solución: Se independizó la tabla de base de datos media de Appwrite de los IDs de archivo reales del Storage. Se ajustó el helper getAppwriteImageUrl para recibir el file_id y el bucket_id correctos mapeados desde el documento de la colección media. También se implementó una detección robusta de tipos de archivo (mimetype) en las acciones del servidor al subir archivos para categorizar correctamente imágenes y videos.

3. Renderizado de Contenido Mixto (Texto Plano con HTML)

  • Problema: Al publicar noticias, las etiquetas HTML guardadas por el editor enriquecido se mostraban en el frontend como texto plano legible (ej. se veía literalmente <p>Texto</p>), o bien el texto plano sin estructura perdía los saltos de línea.
  • Solución: Se integró isomorphic-dompurify junto con la función formatTextToHtml en la vista de detalle. Esta función detecta si el contenido ya contiene etiquetas de estructura HTML. Si las tiene, respeta los elementos (sanitizándolos con un whitelist de seguridad XSS); si es texto plano, convierte inteligentemente los dobles saltos de línea en párrafos (<p>) y saltos individuales en retornos de carro (<br />), garantizando un diseño limpio.

4. Migración de Infraestructura a Coolify y Soporte Multi-dominio

  • Problema: Caídas constantes del servidor en Hostinger debido a limitaciones de memoria y errores 503 en despliegues tradicionales de Node.js.
  • Solución: Se migró el entorno de despliegue a Coolify, una plataforma de self-hosting que gestiona contenedores Docker. Se configuró el enrutamiento para soportar múltiples orígenes y dominios sin errores de cabeceras CORS en el backend, mejorando la disponibilidad global de la web de Almeira.

5. Automatización de Publicaciones vía WhatsApp con IA y Whitelist en Appwrite

  • Arquitectura: Se integró Evolution API con Google Gemini (gemini-3.6-flash) y Appwrite para procesar mensajes y fotos enviados por clientes o salas de prensa directamente en WhatsApp.
  • Almacenamiento en Base de Datos Appwrite: Los chats y clientes autorizados se registran y gestionan directamente en la base de datos de Appwrite (whatsapp_messages con estado whitelist_active/whitelist_paused), permitiendo añadir, pausar o eliminar clientes en tiempo real desde el Dashboard sin necesidad de redesplegar o reiniciar el servidor.
  • Sincronización con Repositorio: Cada modificación realizada en Appwrite se sincroniza automáticamente como respaldo en src/config/whatsapp-chats.json.
  • Resolución Dinámica de Nombres: Al recibir un evento en /api/webhooks/whatsapp, el webhook resuelve el nombre exacto del grupo o remitente consultando Evolution API (con caché en memoria) y valida coincidencias en tiempo real contra los registros activos de Appwrite en src/lib/whatsapp-filter.ts.
  • Gestión Visual en Dashboard: En /dashboard/settings, los administradores pueden añadir nuevos clientes por nombre exacto, pausar/activar chats, o sincronizar automáticamente los grupos detectados en Evolution API con persistencia inmediata en Appwrite.
  • Flujo Editorial y Biblioteca de Medios: Chat autorizado envía mensaje/multimedia -> Si se reciben múltiples imágenes (álbum o ráfaga), el webhook recopila las imágenes candidatas y Google Gemini analiza visualmente cada una para seleccionar la más nítida, profesional y representativa (selected_image_index y image_selection_reason) -> La imagen destacada elegida se sube a Appwrite Storage (media_bucket) y se registra en la Biblioteca de Medios (media), vinculándola como imagen_destacada_id en el post -> Se calculan las dimensiones y previsualizaciones exactas (640x360 px / 16:9 para Noticias; 640x640 px / 1:1 para Eventos y Clasificados) -> Se crea post en Appwrite en estado borrador -> El bot responde al grupo solicitando aprobación e indicando qué foto fue seleccionada -> Administrador responde "Aprobar" -> El post pasa a estado publicado.