Reestructuración pedida por el cliente tras revisar la primera versión. Conversión: - Formulario de 14 campos a 4: nombre, correo, teléfono y mensaje. La clasificación de riesgo ya no se pregunta, se deduce del contexto de la página y viaja oculta. Menos fricción, misma riqueza para el análisis. - El inicio deja de bifurcar y empuja al catálogo: 36 llamados desembocan ahí, y las rutas por industria y escenario pasan a ser la forma de entrar al catálogo ya filtrado, en lugar de competir como destino. - Catálogo rehecho como tienda: barra lateral con filtros, búsqueda que acepta expresiones regulares, títulos que abren la ficha, un solo botón primario por tarjeta y el comparador detrás de un desplegable. - Ficha de producto orientada al cierre: bloque en lenguaje llano sobre si el equipo corresponde al caso, y "Cotizar este equipo" con el formulario precargado. Se retiraron los enlaces que sacaban del embudo a leer normas. - Todos los llamados se unifican en "Recibir cotización". - Widget de WhatsApp: nombre, teléfono, correo y mensaje libre. El texto que escribe la persona es el que viaja a la conversación. Doble audiencia: - 13 escenarios con la iconografía original de FSPM traducen un lugar reconocible a clases de fuego y productos, para quien no domina la terminología. La capa técnica se conserva para búsqueda. - Héroe con fotografía en todas las landings. Correcciones: - El contenedor moría al arrancar porque el CLI de Prisma no podía escribir sus motores; el chown ahora incluye /app/prisma-cli. - Iconos repetidos entre escenarios. El set original no tiene icono de cocina, así que ese escenario usa un marcador tipográfico explícito. - FIREMIKS no aparecía en ningún escenario. - Los ids de campo del formulario colisionaban con dos instancias por página. - Se documentó el despliegue, que faltaba por completo. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
150 lines
6.3 KiB
Markdown
150 lines
6.3 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`. |
|
|
| `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 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.
|