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

165 lines
7.1 KiB
Markdown

# 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.
```bash
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`
```bash
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:
```bash
docker inspect <contenedor> --format '{{json .Mounts}}'
```
## Local con Docker
```bash
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:
```bash
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.
```bash
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.