feat: catálogo como centro del embudo y captura de baja fricción

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]>
This commit is contained in:
Uriel Jareth
2026-08-28 01:30:44 -06:00
co-authored by Claude Opus 5
parent 3c6bdcfc06
commit 0e0331c1fc
580 changed files with 7239 additions and 2683 deletions
+149
View File
@@ -0,0 +1,149 @@
# 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.