Files
fspm-web/docs/DESPLIEGUE.md
T
Uriel JarethandClaude Opus 5 31ee899987 fix: defectos encontrados por la suite de extremo a extremo
La primera corrida completa dio 57 fallas. Solo una parte eran del
producto; el resto era instrumentación mal escrita. Queda en 261 en verde.

Defectos del producto:

- La atribución de campaña se perdía. La captura vivía en un efecto de
  React, así que si la persona llegaba con parámetros de campaña y
  navegaba antes de hidratar, el primer toque nunca se guardaba y
  utmCampaign llegaba vacío al CRM. Ahora se captura en un script en
  línea que corre al analizar el documento.
- Siete páginas sin og:image: una página que declara su propio openGraph
  no hereda las imágenes del layout, las sobreescribe. Solo se nota al
  compartir el enlace. Se resuelve con una función compartida en seo.ts.
- El panel no se podía usar desde Safari por HTTP: la cookie Secure no se
  guarda y el acceso falla en silencio. Queda explícito y documentado.
- El panel se corría de lado en móvil: la cabecera no cabía a 375px.
- Las tarjetas de perfil del acceso se cortaban en móvil. Un elemento de
  rejilla nace con min-width auto y no puede encogerse bajo su contenido.
  Era además el origen de un desplazamiento que se arrastraba al tablero.
- Las tablas accesibles de las gráficas volvían desplazable la página:
  sr-only fija width 1px y el algoritmo de tablas lo ignora.
- El campo de búsqueda del catálogo no tenía nombre accesible.
- Límite de tasa de 5 envíos por IP: bloqueaba una demostración en vivo
  con varias personas en la misma red. Configurable, ahora 20.
- El salto de contenido pasa de left:-9999px a recorte de 1px.

Instrumentación corregida, con la razón escrita en cada caso:

- El desborde horizontal se mide intentando desplazar la página, no con
  documentElement.scrollWidth, que sobreinforma con contenedores propios.
- La comprobación de accesibilidad respeta el árbol de accesibilidad.
- El aislamiento entre roles se prueba pidiendo por URL una oportunidad
  ajena, no comparando folios entre listados paginados.
- Fuera las esperas por networkidle, que agotaban el tiempo sin fallar.
- Que el panel no esté enlazado se comprueba leyendo el HTML servido.
- Safari no enfoca enlaces con Tab: declarado como del navegador.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-28 02:37:42 -06:00

7.1 KiB

Despliegue

Sitio de FSPM con micro CRM. Un solo contenedor: Next.js en modo standalone y SQLite en un volumen. No hay base de datos externa.

Variables de entorno

Variable Requerida Qué pasa si falta
DATABASE_URL Sí Prisma no arranca. En el contenedor ya viene fijada a file:/app/data/fspm.db; no la cambies salvo que muevas el volumen.
AUTH_SECRET Sí El arranque falla con un error explícito. Firma las cookies de sesión del panel.
NEXT_PUBLIC_SITE_URL Sí, en construcción Las URL canónicas, el sitemap y Open Graph apuntan al dominio equivocado. Ver la advertencia de abajo.
NEXT_PUBLIC_WHATSAPP Sí, en construcción El widget de WhatsApp abre una conversación con un número vacío.
DEMO_ACCESO_RAPIDO No Con cualquier valor distinto de false, el acceso de un clic por perfil está activo en /admin/login.
AUTH_COOKIE_INSECURE No Solo para pruebas locales sin TLS. Ver la advertencia de HTTPS abajo. Nunca en producción.
NODE_ENV No Ya viene en production dentro de la imagen.

Advertencia sobre las variables NEXT_PUBLIC_*

Next las sustituye literalmente durante next build, no las lee al arrancar. Cambiar NEXT_PUBLIC_SITE_URL o NEXT_PUBLIC_WHATSAPP en el entorno del contenedor no tiene ningún efecto: hay que reconstruir la imagen pasándolas como --build-arg. Es el error más fácil de cometer aquí, y se manifiesta como un sitemap con el dominio incorrecto y un widget de WhatsApp que no marca a nadie.

docker build \
  --build-arg NEXT_PUBLIC_SITE_URL=https://fspm-demo.urieljareth.org \
  --build-arg NEXT_PUBLIC_WHATSAPP=525540348395 \
  -t fspm-web:demo .

Generar un AUTH_SECRET

node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))"

Uno por entorno. No reutilices el de desarrollo: cualquiera que lo tenga puede firmar una cookie de sesión válida y entrar al panel como administrador.

El panel exige HTTPS

La cookie de sesión va marcada como Secure, que es lo correcto. La consecuencia es que si el panel se sirve por HTTP plano, en Safari y en iOS nadie puede entrar: el navegador se niega a guardar la cookie, el acceso devuelve a la pantalla de inicio de sesión y no dice por qué. Chromium hace una excepción con localhost, así que el problema se ve solo en WebKit y es fácil de atribuir a otra cosa.

Detrás de Coolify con Cloudflare esto no ocurre, porque el tráfico llega por HTTPS. Importa en dos casos: si alguien expone el contenedor por IP y puerto sin TLS, y al probar en local. Para lo segundo existe AUTH_COOKIE_INSECURE=true, que no debe usarse en un entorno accesible desde internet.

El volumen — la parte frágil

Si no montas /app/data, cada redespliegue borra los leads capturados.

La base de demostración se hornea dentro de la imagen durante la construcción. Al arrancar, el punto de entrada la copia al volumen solo si el volumen está vacío; si ya hay una base, no la toca. Ese es el mecanismo que permite que los leads que llegan por el formulario y por el widget sobrevivan a una nueva versión de la imagen.

Sin volumen, el contenedor sigue funcionando —arranca con los datos sembrados—, así que la falla es silenciosa: nadie se entera hasta que un lead real desaparece.

En Coolify: Storages → Add → Volume Mount, con destino /app/data. Verifícalo después del primer despliegue con:

docker inspect <contenedor> --format '{{json .Mounts}}'

Local con Docker

docker build -t fspm-web:demo .

docker run -d --name fspm-demo -p 3100:3000 \
  -v fspm-datos:/app/data \
  -e AUTH_SECRET="<secreto generado>" \
  fspm-web:demo

curl -s http://127.0.0.1:3100/api/salud

/api/salud consulta la base, así que un volumen no escribible se reporta como contenedor enfermo en lugar de pasar por sano.

Coolify

Los endpoints POST /applications/* de la API devuelven 404 en esta instancia (verificado en 4.1.2 y de nuevo en 4.3.12), así que la aplicación se crea desde la interfaz web y solo el despliegue se dispara por API.

  1. Crea la aplicación apuntando al repositorio de Gitea, rama main, tipo Dockerfile.
  2. Fija el dominio, para que Coolify no asigne uno aleatorio y devuelva 503.
  3. Carga las variables de entorno. Las NEXT_PUBLIC_* van también como argumentos de construcción.
  4. Monta el volumen en /app/data.
  5. Despliega y vigila el registro.

Despliegues posteriores:

curl -H "Authorization: Bearer $COOLIFY_TOKEN" \
  "$COOLIFY_API_URL/deploy?uuid=<uuid de la aplicación>"

Verificación posterior al despliegue

Hazla en este orden; cada punto descarta una clase distinta de falla.

BASE=https://fspm-demo.urieljareth.org

curl -s $BASE/api/salud                 # {"ok":true,...}
curl -s $BASE/robots.txt | grep admin   # debe prohibir /admin
curl -s $BASE/sitemap.xml | grep -c admin   # debe dar 0
curl -sI $BASE/icon | grep -i content-type      # image/*
curl -sI $BASE/opengraph-image | grep -i content-type
curl -s $BASE/sitemap.xml | grep -o 'https://[^<]*' | head -3   # ¿el dominio correcto?

Después, a mano:

  1. Envía el formulario del inicio y confirma que devuelve folio.
  2. Abre el widget de WhatsApp, envíalo y revisa que el enlace lleve el prefijo [DEMO], el folio y el mensaje que escribiste.
  3. Entra a /admin/login —no está enlazado desde el sitio, es intencional— y usa el acceso rápido para ver el panel desde los dos roles.
  4. Confirma que la oportunidad de los pasos 1 y 2 aparece en el panel con su campaña y su producto de interés.
  5. Redespliega y vuelve a entrar al panel: los leads de los pasos 1 y 2 deben seguir ahí. Si desaparecieron, el volumen no está montado.

Antes de enseñarlo a un cliente

  • AUTH_SECRET distinto del de desarrollo.
  • DEMO_ACCESO_RAPIDO=false si la demo queda publicada sin acompañamiento: con el acceso rápido activo, cualquiera que llegue a /admin/login entra como administrador.
  • Los datos del panel son ficticios y los precios de los pedidos son inventados. Conviene decirlo en voz alta antes de abrir la pantalla de pedidos.

Notas del entorno de desarrollo

  • El repositorio vive en una carpeta sincronizada a la nube. El cliente de sincronización mueve archivos de .next a media compilación y npm run build falla con Cannot find module './NNNN.js' aunque el código esté correcto. Está reproducido. La construcción dentro de Docker no se ve afectada, porque copia el contexto y compila en el contenedor. Para verificar en local, usa la imagen, no next build.
  • next.config.ts acepta NEXT_DIST_DIR para mover el directorio de salida, pero Next lo resuelve relativo a la raíz del proyecto y no acepta rutas absolutas, así que no resuelve el problema anterior por sí solo.
  • El chown del Dockerfile incluye /app/prisma-cli a propósito: al arrancar, prisma db push reescribe sus motores dentro de su propio node_modules, y sin permiso el contenedor muere con Can't write to /app/prisma-cli/node_modules/@prisma/engines. Se detectó con el contenedor ya construido, nunca durante el build.