Una auditoría marcó `<ul role="img">` en el riel de clases de fuego. El rol
contradice la semántica implícita de lista, así que la combinación es inválida
y el árbol de accesibilidad queda mal formado.
La intención original era correcta: el riel debe anunciarse como una sola
unidad con su resumen, no como "lista de cinco elementos: A, B, C, D, K", que
pierde justo lo que importa, cuáles están encendidas. Lo que estaba mal era el
elemento, no el rol. El riel pasa a un contenedor genérico y conserva
role="img" con su etiqueta.
Al revisar apareció el mismo error en dos gráficas del panel, que además
duplicaban el anuncio: leían el resumen completo y después la tabla accesible
entera. Ahí la representación visual se oculta al árbol y la tabla equivalente
queda como única fuente.
También se corrigió el botón del selector de clases, que se anunciaba "A Clase
A" por tener la letra visible junto a un texto oculto con el nombre completo.
Se agrega una comprobación de regresión que valida el rol contra la semántica
de ul, ol, li, table, nav, main, header y footer, para que no vuelva a
colarse. Suite completa en verde.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Desplegada y verificada de punta a punta contra el dominio público, en
Chromium y en WebKit a 375px: sitio, catálogo filtrado, acceso al panel,
persistencia de sesión y captura de leads con atribución de campaña.
No quedó como recurso de Coolify, y está documentado por qué: la API de esa
instancia no expone creación de aplicaciones (las rutas /applications/*
responden 404 con cuerpo vacío y /openapi.yaml devuelve el HTML del panel),
y al crear un servicio por /services la tarea de despliegue quedó colgada sin
escribir nada en disco. El servidor está sano; el problema es la instancia.
El contenedor corre en el Docker del LXC 102, en la red coolify, con las
etiquetas de Traefik que replican el patrón de las apps que ya sirven con TLS
ahí, y con coolify.managed=false para que nadie lo confunda con un recurso del
panel. La imagen se construye dentro del LXC desde el repo público de Gitea.
El costo de esta ruta está escrito en la documentación: un push no redespliega.
Se agregó el procedimiento de tres comandos para publicar una versión nueva sin
tocar el volumen, y la vía para migrar a app git-based si se consigue el acceso
al panel web.
Se documenta también una carrera que provocó un diagnóstico equivocado: el
acceso usa una acción de servidor, y pulsar el botón antes de que React hidrate
no hace nada ni da error. La primera verificación fallaba solo por el dominio
público y funcionaba contra Traefik directo, lo que parecía señalar a
Cloudflare. No era: los chunks son idénticos por ambos caminos y la hidratación
es igual. Era la espera de la prueba.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
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]>
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]>
Sitio público orientado a venta consultiva e implementación de las
recomendaciones de la auditoría de marca y mercado:
- Inicio con tres rutas (industria, riesgo, cumplimiento) y clasificador
de riesgo en el héroe, contra el hallazgo de que el visitante tenía que
inferir qué solución le correspondía.
- Cinco páginas por segmento prioritario y catálogo comparable por clase
de fuego, capacidad y agente.
- Los claims sin evidencia por modelo quedan aislados y se presentan como
compromiso de documentación, nunca como característica del producto.
- SEO técnico completo: metadatos por página, sitemap derivado de los
datos, JSON-LD, iconos y tarjeta social generadas.
Micro CRM:
- Captura por formulario embebido y por widget de WhatsApp que registra
antes de abrir la conversación.
- Atribución de primer toque que sobrevive a la navegación.
- Deduplicación de contacto; un mismo contacto acumula oportunidades.
- Motor de asignación por reglas con reparto por turno de respaldo.
- Panel con dos roles de alcance distinto, tablero analítico, bitácora,
pedidos y reglas. No enlazado desde el sitio público y con noindex.
Empaquetado en Docker multietapa con la base de demostración horneada en
la imagen y el volumen de datos separado, para que los leads capturados
sobrevivan a los redespliegues.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>