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]>
165 lines
7.1 KiB
Markdown
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.
|