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]>
This commit is contained in:
Uriel Jareth
2026-08-28 02:37:42 -06:00
co-authored by Claude Opus 5
parent 0e0331c1fc
commit 31ee899987
24 changed files with 346 additions and 86 deletions
+15
View File
@@ -12,6 +12,7 @@ SQLite en un volumen. No hay base de datos externa.
| `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_*`
@@ -38,6 +39,20 @@ 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.**